/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.uima.test.junit_extension;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;

import org.apache.uima.cas.CAS;
import org.apache.uima.cas.FSIterator;
import org.apache.uima.cas.Feature;
import org.apache.uima.cas.FeatureStructure;
import org.apache.uima.cas.Type;
import org.apache.uima.cas.impl.LowLevelCAS;
import org.apache.uima.cas.impl.LowLevelTypeSystem;
import org.apache.uima.cas.text.AnnotationFS;
import org.apache.uima.collection.CasConsumer;
import org.apache.uima.collection.CasConsumer_ImplBase;
import org.apache.uima.resource.ResourceConfigurationException;
import org.apache.uima.resource.ResourceInitializationException;
import org.apache.uima.resource.ResourceProcessException;
import org.apache.uima.util.ProcessTrace;

/**
 * The AnnotationWriter class writes specified annotations to an output file.
 * The encoding of the output file is UTF-8
 */

public class AnnotationWriter extends CasConsumer_ImplBase implements CasConsumer
{
	//output file
	private File outFile;
	//output file writer
	private OutputStreamWriter fileWriter;
	//respected annotations
	private String[] tofs;
	//check if reconfigure must be called
	private boolean reconfig = false;
	
	private final static String featureOnlyKey = "feature";

	/**
	 * Initializes this CAS Consumer with the parameters specified in the 
	 * descriptor.
	 * 
	 * @throws ResourceInitializationException if there is error in 
	 * initializing the resources
	 */
	public void initialize() throws ResourceInitializationException
	{

		// extract configuration parameter settings
		String oPath = (String) getUimaContext().getConfigParameterValue("outputFile");

		//Output file should be specified in the descriptor
		if (oPath == null)
		{
			//set reconfiguration - reconfig() must be called
			this.reconfig = true;
		}
		else
		{
			// If specified output directory does not exist, try to create it
			this.outFile = new File(oPath);
			if (this.outFile.getParentFile() != null && !this.outFile.getParentFile().exists())
			{
				if (!this.outFile.getParentFile().mkdirs())
					throw new ResourceInitializationException(
						ResourceInitializationException.RESOURCE_DATA_NOT_VALID,
						new Object[] { oPath, "outputFile" });
			}
			try
			{
				this.fileWriter = new OutputStreamWriter(new FileOutputStream(this.outFile, false), "UTF-8");
			}
			catch (IOException e)
			{
				throw new ResourceInitializationException(e);
			}
		}

		//extract annotation types
		this.tofs = (String[]) getUimaContext().getConfigParameterValue("AnnotationTypes");
		//sort array
		if (this.tofs != null)
			Arrays.sort(this.tofs);

	}

	/**
	 * processTofs() writes als specified types an features to a HashMap.
	 * 
	 * @param aCAS a CAS with a TypeSystem
	 * @param tofs respected tofs
	 * @return HashMap - Map with all types an features.
	 */
	private HashMap processTofs(CAS aCAS, String[] someTofs)
	{
		HashMap types = new HashMap(10);

		for (int i = 0; i < someTofs.length; i++)
		{
			Type type = aCAS.getTypeSystem().getType(someTofs[i]);

			if (type == null) //maybe a feature
			{
				int index = someTofs[i].indexOf(":");

				if (index != -1)
				{
					String typename = someTofs[i].substring(0, index);
					Type typeKey = aCAS.getTypeSystem().getType(typename);

					//get feature object (Vector) for the current type
					Object obj = types.get(typeKey);

					//if type is not included in the typelist create type and add feature
					if (obj == null)
					{
						ArrayList list = new ArrayList(10);
						Feature fs = aCAS.getTypeSystem().getFeatureByFullName(someTofs[i]);
						list.add(0, featureOnlyKey);
						list.add(fs);
						types.put(typeKey, list);
					}
					else //add feature to type
						{
						//cast feature vector for the current type
						ArrayList vec = (ArrayList) obj;
						Feature fs = aCAS.getTypeSystem().getFeatureByFullName(someTofs[i]);
						vec.add(fs);
					}

				}
			}
			else
			{
				//add type as key and a Vector as Feature container 
				if(types.containsKey(type)){
					ArrayList featureList = (ArrayList) types.get(type);
					if(featureList.size() >0 && featureList.get(0).equals(featureOnlyKey)){
						featureList.remove(0);
					}
					// type is already in the list do not overwrite it!
				}else{
					types.put(type, new ArrayList(10));
				}
			}
		}

		return types;
	}

	public synchronized void processCas(CAS aCAS) throws ResourceProcessException
	{
		if (this.reconfig == true)
		{
			throw new ResourceProcessException(
				ResourceInitializationException.CONFIG_SETTING_ABSENT,
				new Object[] { "outputFile" });
		}

		//get low level CAS
		LowLevelCAS ll_cas = aCAS.getLowLevelCAS();

		//get low level TypeSystem
		LowLevelTypeSystem ll_typeSystem = ll_cas.ll_getTypeSystem();

		//get types and feature interessted in
		HashMap types = processTofs(aCAS, this.tofs);

		try
		{
			//iterate and print annotations
			FSIterator typeIterator = aCAS.getAnnotationIndex().iterator();

			for (typeIterator.moveToFirst(); typeIterator.isValid(); typeIterator.moveToNext())
			{
				Iterator it = types.keySet().iterator();

				while (it.hasNext())
				{
					//get current type and features
					Type currentType = (Type) it.next();
					boolean isFeatureOnly = false;
					
					ArrayList featureList = (ArrayList) types.get(currentType);
					if(featureList.size() >0 && featureList.get(0).equals(featureOnlyKey)){
						featureList.remove(0);
						isFeatureOnly = true;
					}
					Feature[] features = (Feature[]) featureList.toArray(new Feature[] {
					});
					
					AnnotationFS annot = (AnnotationFS) typeIterator.get();

					if (annot.getType().getName() == currentType.getName())
					{
						//only for formatting necessary
						boolean firstFeature = true;

						String span = annot.getCoveredText();
						if(!isFeatureOnly){
							this.fileWriter.write(
								annot.getType().getShortName()
									+ "(" + annot.getBegin() + "," + annot.getEnd()	+ "): "	+ span);
						}else{
							this.fileWriter.write(
								annot.getType().getShortName()
									+ ": ");
						}

						for (int f = 0; f < features.length; f++)
						{
							if (firstFeature)
							{
								this.fileWriter.write("  { ");
								firstFeature = false;
							}
							else
							{
								this.fileWriter.write(", ");
							}

							Feature fs = features[f];
							int typeClass = ll_cas.ll_getTypeClass(ll_typeSystem.ll_getCodeForType(fs.getRange()));
							this.fileWriter.write(fs.getShortName() + "=");
							
							switch (typeClass)
							{
								case LowLevelCAS.TYPE_CLASS_FLOAT :
									this.fileWriter.write(Float.toString(annot.getFloatValue(fs)));
									break;

								case LowLevelCAS.TYPE_CLASS_INT :
									this.fileWriter.write(Integer.toString(annot.getIntValue(fs)));
									break;

								case LowLevelCAS.TYPE_CLASS_STRING :
									String value = annot.getStringValue(fs);
									if(value != null) {
										this.fileWriter.write(value);	
									} else {
										this.fileWriter.write("null");
									}
									break;
								
								case LowLevelCAS.TYPE_CLASS_FS:
								
									FeatureStructure fStruct = annot.getFeatureValue(fs);
									if(fStruct != null) {
										this.fileWriter.write(fStruct.toString());	
									} else {
										this.fileWriter.write("null");
									}
									break;
							}
						}

						if (firstFeature == false)
						{
							this.fileWriter.write(" }");
						}

						this.fileWriter.write(System.getProperty("line.separator"));
					}

				}
			}

			this.fileWriter.flush();
		}
		catch (Exception ex)
		{
			throw new ResourceProcessException(ex);
		}

	}

	public void batchProcessComplete(ProcessTrace aTrace) throws ResourceProcessException, IOException
	{
		 // nothing to do here 	
	}

	public void collectionProcessComplete(ProcessTrace aTrace) throws ResourceProcessException, IOException
	{
		if (this.fileWriter != null)
		{
			this.fileWriter.close();
		}
	}

	public void reconfigure() throws ResourceConfigurationException
	{
		//reset reconfiguration - is done
		this.reconfig = false;

		super.reconfigure();
		// extract configuration parameter settings
		String oPath = (String) getUimaContext().getConfigParameterValue("outputFile");
		File oFile = new File(oPath);
		//if output file has changed, close exiting file and open new
		if (!oFile.equals(this.outFile))
		{
			this.outFile = oFile;
			try
			{
				if (this.fileWriter != null)
					this.fileWriter.close();

				// If specified output directory does not exist, try to create it
				if (oFile.getParentFile() != null && !oFile.getParentFile().exists())
				{
					if (!oFile.getParentFile().mkdirs())
						throw new ResourceConfigurationException(
							ResourceInitializationException.RESOURCE_DATA_NOT_VALID,
							new Object[] { oPath, "outputFile" });
				}
				//write result specification to the output file
				this.fileWriter = new OutputStreamWriter(new FileOutputStream(oFile, false), "UTF-8");
			}
			catch (IOException e)
			{
				throw new ResourceConfigurationException();
			}
		}

		//extract annotation types
		this.tofs = (String[]) getUimaContext().getConfigParameterValue("AnnotationTypes");
		//sort array
		if (this.tofs != null)
			Arrays.sort(this.tofs);

	}

	public void destroy()
	{
		if (this.fileWriter != null)
		{
			try
			{
				this.fileWriter.close();
			}
			catch (IOException e)
			{
				// ignore IOException on destroy
			}
		}
	}

}
