package com.esri.geoevent.solutions.adapter.cot;

/*
 * #%L
 * CoTAdapter.java - Esri :: AGES :: Solutions :: Adapter :: CoT - Esri - 2013
 * org.codehaus.mojo-license-maven-plugin-1.5
 * $Id: update-file-header-config.apt.vm 17764 2012-12-12 10:22:04Z tchemit $
 * $HeadURL: https://svn.codehaus.org/mojo/tags/license-maven-plugin-1.5/src/site/apt/examples/update-file-header-config.apt.vm $
 * %%
 * Copyright (C) 2013 - 2014 Esri
 * %%
 * Licensed 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.
 * #L%
 */


//import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.nio.ByteBuffer;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.JsonParser;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import com.esri.core.geometry.Geometry;
import com.esri.core.geometry.GeometryEngine;
import com.esri.core.geometry.MapGeometry;
import com.esri.core.geometry.SpatialReference;
import com.esri.ges.adapter.AdapterDefinition;
import com.esri.ges.adapter.InboundAdapterBase;
import com.esri.ges.core.ConfigurationException;
import com.esri.ges.core.component.ComponentException;
import com.esri.ges.core.geoevent.FieldDefinition;
import com.esri.ges.core.geoevent.FieldException;
import com.esri.ges.core.geoevent.FieldGroup;
import com.esri.ges.core.geoevent.FieldType;
import com.esri.ges.core.geoevent.GeoEvent;
import com.esri.ges.core.geoevent.GeoEventDefinition;
import com.esri.ges.core.property.Property;
import com.esri.core.geometry.Point;

public class CoTAdapter extends InboundAdapterBase
{
	private static final Log log = LogFactory.getLog(CoTAdapter.class);

	private static final int GCS_WGS_1984 = 4326;
	private String guid;

	// this ArrayList contains ALL the type defs but it must be used differently
	// depending on what you are looking for.
	private ArrayList<CoTTypeDef> coTTypeMap;

	private ByteArrayOutputStream jsonBuffer;
	private JsonGenerator generator;
	private JsonFactory factory;
	private boolean firstVertex;
	private double cachedLat;
	private double cachedLon;
	private double cachedHae;
	private static final int CAPACITY = 1 * 1024 * 1024;
	private HashMap<String,String> buffers = new HashMap<String,String>();
	private int maxBufferSize;

	private SAXParserFactory saxFactory;
	@SuppressWarnings("unused")
	private SAXParser saxParser;
	@SuppressWarnings("unused")
	private MessageParser messageParser;



	public CoTAdapter(AdapterDefinition adapterDefinition, String guid) throws ConfigurationException, ComponentException
	{
		super(adapterDefinition);

		this.guid = guid;

		messageParser = new MessageParser(null);
		saxFactory = SAXParserFactory.newInstance();
		try
		{
			saxParser = saxFactory.newSAXParser();
		} catch (ParserConfigurationException e)
		{
			e.printStackTrace();
			saxParser = null;
		} catch (SAXException e)
		{
			e.printStackTrace();
			saxParser = null;
		}

	}

	@Override
	public void afterPropertiesSet()
	{
		if (hasProperty(CoTAdapterService.MAXIMUM_BUFFER_SIZE_LABEL))
		{
			Property bufSizeProperty = getProperty(CoTAdapterService.MAXIMUM_BUFFER_SIZE_LABEL);
			if( bufSizeProperty != null )
			{
				int maxBufferSize = (Integer)bufSizeProperty.getValue();
				if( maxBufferSize <= 0 )
				{
					log.error("Cannot set the maximum buffer size to " + maxBufferSize );
				}
				else
					this.maxBufferSize = maxBufferSize;
			}
		}
		if (hasProperty(CoTAdapterService.COT_TYPES_PATH_LABEL))
		{
			Property cotTypesPathProperty = getProperty(CoTAdapterService.COT_TYPES_PATH_LABEL);
			if( cotTypesPathProperty != null )
			{
				String userDefinedPath = cotTypesPathProperty.getValueAsString();
				if( userDefinedPath != null && (!userDefinedPath.equals("")))
				{
					try
					{
						this.coTTypeMap = CoTUtilities.getCoTTypeMap(new FileInputStream(((Property) cotTypesPathProperty).getValueAsString()));
						log.info("CotTypes.xml path will be set to: "	+ userDefinedPath);
					} catch (Exception e)
					{
						this.coTTypeMap = null;
						log.error("Problem loading the user-specified CoTTypes.xml file.",e);
					}
				}
			}
		}
		if( this.coTTypeMap == null )
		{
			try
			{
				String defaultPath = "CoTTypes/CoTtypes.xml";
				this.coTTypeMap = CoTUtilities.getCoTTypeMap(this.getClass().getClassLoader().getResourceAsStream(defaultPath));
				log.info("Default CoTtypes.xml definitions were loaded successfully.");
			} catch (Exception e1)
			{
				log.error("Problem loading the default CoTTypes.xml file.",e1);
			}
		}

	}

	@Override
	public void receive(ByteBuffer buf, String channelId)
	{
		buf.mark();
		int size = buf.remaining();
		if (size < 1)
			return;
		byte[] data = new byte[size];
		buf.get(data, 0, size);
		//System.out.println(" \n");
		//System.out.println("Read " + size + " bytes");

		String xml = new String(data);
		parseUsingDocument(xml,channelId);
		//parseUsingStream(buf);
	}

	//Might need this block for future enhancements...
	/*private void parseUsingStream( ByteBuffer bb )
	{
		try
		{
			int remaining = bb.remaining();
			if( remaining <= 0 )
				return;
			byte[] bytes = new byte[remaining];
			bb.get(bytes);
			saxParser.parse( new ByteArrayInputStream(bytes), messageParser);
			bytes = null;
		} catch (SAXException e)
		{
			e.printStackTrace();
		} catch (IOException e)
		{
			e.printStackTrace();
		}
	}*/

	private void parseUsingDocument( String xml, String channelId )
	{
		if( buffers.containsKey(channelId) )
		{
			String temp = buffers.remove(channelId);
			temp = temp + xml;
			if( temp.length() > maxBufferSize )
			{
				log.error("The size of the incoming xml message exceeds the configured maximum buffer size of "+maxBufferSize+".  The buffer contents will be discarded to make room for incoming data.");
				temp = scanForEvent(temp);  // Look for something that looks like a new message.
				if( temp == null ) // If we didn't find something that looks like a new message, just start with the xml that was passed in (discarding the buffered data).
					temp = xml;
			}
			xml = temp;
		}
		try
		{
			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
			DocumentBuilder db = dbf.newDocumentBuilder();
			InputSource source = new InputSource();
			source.setCharacterStream(new StringReader(xml));
			Document doc = db.parse(source);
			NodeList nodeList = doc.getElementsByTagName("event");

			if (nodeList != null)
			{
				for( int i = 0; i < nodeList.getLength(); i++ )
				{
					GeoEvent msg = geoEventCreator.create(guid);
					Element e = (Element) nodeList.item(i);

					String version = e.getAttribute("version");
					if (!version.isEmpty())
						msg.setField(0, Double.valueOf(version));

					msg.setField(1, e.getAttribute("uid"));

					String type = e.getAttribute("type");
					msg.setField(2, type);
					msg.setField(3, CoTUtilities.getSymbolFromCot(type));
					//System.out.println("2525b from type: "+ CoTUtilities.getSymbolFromCot(type));

					//System.out.println("type: " + e.getAttribute("type") " -> " + convertType(e.getAttribute("type")));
					msg.setField(4, convertType(e.getAttribute("type")));

					//System.out.println("how: " + e.getAttribute("how") + " -> "+ convertHow(e.getAttribute("how")));
					msg.setField(5, e.getAttribute("how"));
					msg.setField(6, convertHow(e.getAttribute("how")));

					msg.setField(7, parseCoTDate(e.getAttribute("time")));
					msg.setField(8, parseCoTDate(e.getAttribute("start")));
					msg.setField(9, parseCoTDate(e.getAttribute("stale")));

					msg.setField(10, e.getAttribute("access"));
					msg.setField(11, e.getAttribute("opex"));
					msg.setField(12, convertOpex(e.getAttribute("opex")));
					//System.out.println("opex: " + e.getAttribute("opex" + " is a:  " + convertOpex(e.getAttribute("opex")));

					msg.setField(13, e.getAttribute("qos"));
					msg.setField(14, convertQos(e.getAttribute("qos")));
					//System.out.println("qos: " + e.getAttribute("qos") + " -> " convertQos(e.getAttribute("qos")));

					NodeList points = e.getElementsByTagName("point");
					if (points.getLength() > 0)
					{
						Element pointElement = (Element) points.item(0);
						MapGeometry geom = createGeometry(pointElement);
						msg.setField(15, geom);
						//msg.setField( 15, geom.toJson() );
					}

					GeoEventDefinition ged = msg.getGeoEventDefinition();
					traverseBranch( findChildNodes(e, "detail" ).get(0), msg, ged.getFieldDefinition("detail") );

					geoEventListener.receive( msg );
				}
			}
		} catch (Exception e)
		{
			if( e.getMessage() != null && e.getMessage().equals("XML document structures must start and end within the same entity.") )
			{
				if( xml != null )
					buffers.put(channelId, xml);
				return;
			}
			log.error("Error while parsing CoT message.  For details, set log level to DEBUG.",e);
			log.debug("Error while parsing the message : "+xml);
			return;
		}
	}

	private String scanForEvent(String buffer)
	{
		int index = buffer.indexOf( "<?xml version'", 1 );
		if( index > 0 )
			return buffer.substring(index);
		return null;
	}

	@SuppressWarnings("incomplete-switch")
	private void traverseBranch(Node node, FieldGroup fieldGroup, FieldDefinition fieldDefinition) throws FieldException
	{
		if( node == null ) return;
		//System.out.println("Examining node named \""+node.getNodeName()+"\"");
		FieldType fieldType = fieldDefinition.getType();
		switch( fieldType )
		{
		case Group:
			FieldGroup childFieldGroup = fieldGroup.createFieldGroup(fieldDefinition.getName());
			fieldGroup.setField( fieldDefinition.getName(), childFieldGroup);
			for( FieldDefinition childFieldDefinition : fieldDefinition.getChildren() )
			{
				String childName = childFieldDefinition.getName();
				List<Node> childNodes = findChildNodes( node, childName );
				if( childNodes.size() > 0 )
				{
					for( Node childNode : childNodes )
						traverseBranch( childNode, childFieldGroup, childFieldDefinition );
				}
				else
					traverseBranch( node, childFieldGroup, childFieldDefinition );
			}
			break;
		case String:
			String value = getAttribute( node, fieldDefinition.getName() );
			if( value != null )
				fieldGroup.setField(fieldDefinition.getName(), value);
			break;
		case Integer:
			value = getAttribute( node, fieldDefinition.getName() );
			if( value != null )
				fieldGroup.setField(fieldDefinition.getName(), new Integer(value));
			break;
		case Double:
			value = getAttribute( node, fieldDefinition.getName() );
			if( value != null )
				fieldGroup.setField(fieldDefinition.getName(), new Double(value));
			break;
		case Boolean:
			value = getAttribute( node, fieldDefinition.getName() );
			if( value != null )
				fieldGroup.setField(fieldDefinition.getName(), new Boolean(value));
			break;
		case Date:
			value = getAttribute( node, fieldDefinition.getName() );
			if( value != null )
			{
				Date date = new Date();
				try
				{
					date = parseCoTDate(value);
				}catch(Exception ex)
				{
				}
				fieldGroup.setField(fieldDefinition.getName(), date);
			}
			break;
		case Geometry:
			MapGeometry geometry = createGeometry(node);
			if( geometry != null )
				fieldGroup.setField(fieldDefinition.getName(), geometry);
			break;
		case Long:
			value = getAttribute( node, fieldDefinition.getName() );
			if( value != null )
				fieldGroup.setField(fieldDefinition.getName(), new Long(value));
			break;
		case Short:
			value = getAttribute( node, fieldDefinition.getName() );
			if( value != null )
				fieldGroup.setField(fieldDefinition.getName(), new Integer(value));
			break;
		}
	}

	private List<Node> findChildNodes( Node node, String childName)
	{
		ArrayList<Node> children = new ArrayList<Node>();
		NodeList childNodes = node.getChildNodes();
		for( int i = 0; i < childNodes.getLength(); i++ )
		{
			Node child = childNodes.item(i);
			if( child.getNodeName().equals(childName) )
				children.add(child);
		}
		return children;
	}

	private String getAttribute( Node node, String attributeName )
	{
		NamedNodeMap attributes = node.getAttributes();
		for(int i = 0; i < attributes.getLength(); i++ )
		{
			Node attributeNode = attributes.item(i);
			if( attributeNode.getNodeName().equals(attributeName))
			{
				return attributeNode.getNodeValue();
			}
		}
		return null;
	}

	private String convertQos(String type) {

		StringBuilder sb = new StringBuilder();

		Matcher matcher;
		for (CoTTypeDef cd : this.coTTypeMap) {
			if (cd.isPredicate() && cd.getValue().startsWith("q.")) {
				Pattern pattern = Pattern.compile(cd.getKey());
				matcher = pattern.matcher(type);
				if (matcher.find()) {

					sb.append(cd.getValue() + " ");

				}

			}

		}

		return this.filterOutDots(sb.toString());

	}

	private String convertOpex(String type) {

		Matcher matcher;
		for (CoTTypeDef cd : this.coTTypeMap) {
			if (cd.isPredicate() && cd.getValue().startsWith("o.")) {
				Pattern pattern = Pattern.compile(cd.getKey());
				matcher = pattern.matcher(type);
				if (matcher.find()) {

					return this.filterOutDots(cd.getValue());

				}

			}

		}
		// no match was found
		return "";

	}

	private String convertHow(String type) {

		Matcher matcher;
		for (CoTTypeDef cd : this.coTTypeMap) {
			if (!cd.isPredicate()) {
				Pattern pattern = Pattern.compile(cd.getKey());
				matcher = pattern.matcher(type);
				if (matcher.find()) {

					return this
							.filterOutDots(appendToHow(type) + cd.getValue());

				}

			}

		}
		// no match was found
		return "";

	}

	private String appendToHow(String type) {
		Matcher matcher;
		StringBuffer sb = new StringBuffer();
		for (CoTTypeDef cd : this.coTTypeMap) {
			// now only consider the value if it is:
			// 1. a predicate
			if (cd.isPredicate()) {
				Pattern pattern = Pattern.compile(cd.getKey());
				matcher = pattern.matcher(type);
				if (matcher.find()) {
					// now only append the value if it is:
					// 1. not prefixed with a dot notation
					if (cd.getValue().startsWith("h.")) {
						sb.append(cd.getValue() + " ");
					}
				}
			}
		}

		return sb.toString();
	}

	private String convertType(String type) {

		Matcher matcher;
		for (CoTTypeDef cd : this.coTTypeMap) {
			if (!cd.isPredicate()) {
				Pattern pattern = Pattern.compile(cd.getKey());
				matcher = pattern.matcher(type);
				if (matcher.find()) {

					return this.filterOutDots(appendToType(type)
							+ cd.getValue());

				}

			}

		}
		// no match was found
		return "";

	}

	private String appendToType(String type) {
		Matcher matcher;
		StringBuffer sb = new StringBuffer();
		for (CoTTypeDef cd : this.coTTypeMap) {
			// now only consider the value if it is:
			// 1. a predicate
			// 2. not prefixed with a dot notation
			if (cd.isPredicate()
					&& !(cd.getValue().startsWith("h.")
							|| cd.getValue().startsWith("t.")
							|| cd.getValue().startsWith("r.")
							|| cd.getValue().startsWith("q.") || cd.getValue()
							.startsWith("o."))) {
				Pattern pattern = Pattern.compile(cd.getKey());
				matcher = pattern.matcher(type);
				if (matcher.find()) {

					sb.append(cd.getValue() + " ");

				}
			}
		}

		return sb.toString();
	}

	/*
	 * The following method's only purpose is to take out the dot notations eg:
	 * "h.whatever" will indicate that "whatever" is in the "how" category
	 */
	private String filterOutDots(String s) {
		String sStageOne = s.replace("h.", "").replace("t.", "")
				.replace("r.", "").replace("q.", "").replace("o.", "");

		String[] s2 = sStageOne.trim().split(" ");

		ArrayList<String> l1 = new ArrayList<String>();
		for (String item : s2) {
			l1.add(item);

		}
		ArrayList<String> l2 = new ArrayList<String>();

		Iterator<String> iterator = l1.iterator();

		while (iterator.hasNext()) {
			String o = (String) iterator.next();
			if (!l2.contains(o))
				l2.add(o);
		}

		StringBuffer sb = new StringBuffer();
		for (String item : l2) {
			sb.append(item);
			sb.append(" ");

		}

		return sb.toString().trim().toLowerCase();
	}

	public Date parseCoTDate(String dateString) throws Exception
	{
		if (!dateString.isEmpty())
		{
			DateFormat formatter1 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SS'Z'");
			formatter1.setTimeZone(TimeZone.getTimeZone("Zulu"));
			DateFormat formatter2 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
			formatter2.setTimeZone(TimeZone.getTimeZone("Zulu"));
			Date date = null;
			try
			{
				if( date == null )
					date = (Date) formatter1.parse(dateString);
			}catch( ParseException ex )
			{
			}
			try
			{
				if( date == null )
					date = (Date) formatter2.parse(dateString);
			}catch( ParseException ex )
			{
			}
			return date;
		}
		return null;
	}

	// temp code to show the details field
	public static String elementToString(Node n) {

		String name = n.getNodeName();
		short type = n.getNodeType();

		if (Node.CDATA_SECTION_NODE == type) {
			return "<![CDATA[" + n.getNodeValue() + "]]&gt;";
		}

		if (name.startsWith("#")) {
			return "";
		}

		StringBuffer sb = new StringBuffer();
		sb.append('<').append(name);

		NamedNodeMap attrs = n.getAttributes();
		if (attrs != null) {
			for (int i = 0; i < attrs.getLength(); i++) {
				Node attr = attrs.item(i);
				sb.append(' ').append(attr.getNodeName()).append("=\"")
				.append(attr.getNodeValue()).append("\"");
			}
		}

		String textContent = null;
		NodeList children = n.getChildNodes();

		if (children.getLength() == 0) {
			if ((textContent = n.getTextContent()) != null
					&& !"".equals(textContent)) {
				sb.append(textContent).append("</").append(name).append('>');
			} else {
				sb.append("/>").append('\n');
			}
		} else {
			sb.append('>').append('\n');
			boolean hasValidChildren = false;
			for (int i = 0; i < children.getLength(); i++) {
				String childToString = elementToString(children.item(i));
				if (!"".equals(childToString)) {
					sb.append(childToString);
					hasValidChildren = true;
				}
			}

			if (!hasValidChildren
					&& ((textContent = n.getTextContent()) != null)) {
				sb.append(textContent);
			}

			sb.append("</").append(name).append('>');
		}

		return sb.toString();
	}

	private MapGeometry createGeometry(Node node)
	{
		if( node.getNodeName().equals("point") )
		{
			String lat = getAttribute(node,"lat");
			String lon = getAttribute(node, "lon");
			String hae = getAttribute(node, "hae");
			Point pt = new Point();
			if (!lat.isEmpty() && !lon.isEmpty()) {
				if (hae.isEmpty()) {
					// SpatialReference sr =
					pt.setX(Double.valueOf(lon));
					pt.setY(Double.valueOf(lat));
					SpatialReference srOut = SpatialReference.create(4326);
					MapGeometry mapGeo = new MapGeometry(pt, srOut);
					return mapGeo;

				} else {
					pt.setX(Double.valueOf(lon));
					pt.setY(Double.valueOf(lat));
					pt.setZ(Double.valueOf(hae));
					SpatialReference srOut = SpatialReference.create(4326);
					MapGeometry mapGeo = new MapGeometry(pt, srOut);
					return mapGeo;
				}

			}
		}

		if( node.getNodeName().equals("shape") )
		{
			try
			{
				List<Node> polylines = findChildNodes( node, "polyline" );
				for( Node polyline : polylines)
				{
					startNewPolygon();
					List<Node> vertices = findChildNodes( polyline, "vertex" );
					for( Node vertex : vertices )
					{
						double lat = 0;
						double lon = 0;
						double hae = 0;
						String s = null;

						s = getAttribute( vertex, "lat");
						if( s != null )
							lat = Double.parseDouble( s );

						s = getAttribute( vertex, "lon");
						if( s != null )
							lon = Double.parseDouble( s );

						s = getAttribute( vertex, "hae");
						if( s != null )
							hae = Double.parseDouble( s );

						addVertexToPolygon( lat, lon, hae );

					}
					String geometryString = closePolygon();
					String jsonString = geometryString.substring(0, geometryString.length()-1) + ",\"spatialReference\":{\"wkid\":"+GCS_WGS_1984+"}}";
					//System.out.println("json string = \""+jsonString+"\"");
					JsonFactory jf = new JsonFactory();
					JsonParser jp = jf.createJsonParser(jsonString);
					MapGeometry geometry = GeometryEngine.jsonToGeometry(jp);
					//Geometry geometry = spatial.fromJson(jsonString);
					return geometry;
				}
			}catch(Exception ex)
			{
				generator = null;
				return null;
			}
		}
		return null;
	}

	private void startNewPolygon() throws IOException
	{
		if( generator == null )
			initializeJsonGenerator();
		generator.writeStartObject();
		generator.writeArrayFieldStart("rings");
		generator.writeStartArray();
		firstVertex = true;
	}

	private void initializeJsonGenerator() throws IOException
	{
		factory = new JsonFactory();
		jsonBuffer = new ByteArrayOutputStream(CAPACITY);
		generator = factory.createJsonGenerator(jsonBuffer);
	}

	private void addVertexToPolygon(double lat, double lon, double hae) throws JsonGenerationException, IOException
	{
		if( firstVertex )
		{
			firstVertex = false;
			cachedLat = lat;
			cachedLon = lon;
			cachedHae = hae;
			firstVertex = false;
		}

		generator.writeStartArray();
		generator.writeNumber(lon);
		generator.writeNumber(lat);
		//generator.writeNumber(hae);
		generator.writeEndArray();
	}

	private String closePolygon() throws IOException
	{
		if(!firstVertex)
			addVertexToPolygon( cachedLat, cachedLon, cachedHae );
		generator.writeEndArray();
		generator.writeEndArray();
		generator.writeEndObject();
		generator.flush();
		String jsonString = jsonBuffer.toString();
		jsonBuffer.reset();
		return jsonString;
	}

	@Override
	protected GeoEvent adapt(ByteBuffer buffer, String channelId)
	{
		// Do nothing, this will not be called since we have overloaded the receive() function.
		return null;
	}

}