/**
 * Marshaller.java
 * Created May 1, 2006
 * Copyright (C) Tandberg Television 2006
 */
package com.tandbergtv.workflow.message.util;

import java.util.List;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import com.tandbergtv.workflow.comm.HTTPDevice;
import com.tandbergtv.workflow.comm.IDestination;
import com.tandbergtv.workflow.comm.IDevice;
import com.tandbergtv.workflow.comm.ISource;
import com.tandbergtv.workflow.comm.TCPDevice;
import com.tandbergtv.workflow.message.WPCLCommand;
import com.tandbergtv.workflow.message.WorkflowMessage;
import com.tandbergtv.workflow.message.WorkflowPayload;

/**
 * Marshals a workflow message to the predefined internal format. Does not
 * perform schema validation.
 * 
 * @author Sahil Verma
 * @author Vlada Jakobac
 */
public final class Marshaller
{
	private static final Logger logger = Logger.getLogger(Marshaller.class);

	/**
	 * Default ctor
	 */
	private Marshaller()
	{
		super();
	}

	public static Marshaller newMarshaller()
	{
		return new Marshaller();
	}

	/**
	 * Marshals the specified message into a well-formed DOM
	 * 
	 * @param message The WorkflowMessage to marshal
	 * @return The Marshalled document
	 * @throws MarshalException
	 */
	public Document marshal(WorkflowMessage message) throws MarshalException
	{
		Document document = null;
		String type = message.getType().toString();
		String uid = message.getMessageUID().getUID();
		String requestKey = new String();
		
		if (message.getKey() != null)
			requestKey = message.getKey().toString();

		try
		{
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			factory.setNamespaceAware(true);
			
			document = factory.newDocumentBuilder().newDocument();
			
			Element root = createRootElement(document, uid, type, requestKey);
			document.appendChild(root);
			
			this.marshalProperties(document, message);
			
			if (message.getCommand() != null)
			{
				marshalCommand(message.getCommand(), document);
			}

			if (message.getPayload() != null)
			{
				WorkflowPayload payload = (WorkflowPayload) message.getPayload();
				marshalPayload(root, payload);
			}
		}
		catch (Exception e)
		{
			throw new MarshalException("Failed to marshal the document", e);
		}
		
		return document;
	}

	/**
	 * @param document
	 * @param uid
	 * @param type
	 * @param requestKey
	 * @return
	 * 
	 * 
	 * creates the root element from uid, type and request key
	 */
	private Element createRootElement(Document document, String uid, String type, String requestKey)
	{
		Element root = createElement(document, WFSMessageConstants.ROOT);

		root.setAttribute(WFSMessageConstants.UID, uid);
		root.setAttribute(WFSMessageConstants.TYPE, type);
		root.setAttribute(WFSMessageConstants.REQUEST_KEY, requestKey);

		Element body = createElement(document, WFSMessageConstants.BODY);

		Element params = createElement(document, WFSMessageConstants.PARAMETERS);
		body.appendChild(params);

		root.appendChild(body);

		return root;
	}
	
	private void marshalPayload(Element root, WorkflowPayload payload) throws XPathExpressionException
	{
		XPath xpath = XPathFactory.newInstance().newXPath();
		Node parameterList = (Node) xpath.evaluate(WFSMessageConstants.PARAMETERS_XPATH, root, XPathConstants.NODE);
		
		for (AbstractTree absTree : ((Tree)payload.getPayload()).getTreeList())
		{
			if (absTree instanceof Tree)
				marshalTree(parameterList, (Tree) absTree);
		}
	}
	
	private void marshalTree(Node element, Tree tree) 
	{
		Document document = element.getOwnerDocument();
		String key = tree.getKey();
		List<AbstractTree> treeList = tree.getTreeList();
		
		if (treeList == null)
			return;
		
		if (treeList.size() > 1 || (treeList.size() == 1 && ! (treeList.get(0) instanceof Scalar)))
		{
			//it's another tree, not a scalar
			Element listParam = createElement(document, WFSMessageConstants.LIST);//<List>
			listParam.setAttribute(WFSMessageConstants.LIST_NAME, key);
			element.appendChild(listParam);
			
			for (AbstractTree absTree : treeList)
			{
				if (absTree instanceof Tree)
				{
					Tree listItemTree = (Tree) absTree;
					String listItemKey = listItemTree.getKey();
					Element listItemParam = createElement(document, WFSMessageConstants.LIST_ITEM);//<ListItem>
					
					listItemParam.setAttribute(WFSMessageConstants.LIST_ITEM_NAME, listItemKey);
					listParam.appendChild(listItemParam);
					
					for (AbstractTree absSubTree : listItemTree.getTreeList())
					{
						if (absSubTree instanceof Scalar)
							marshalScalar(listItemParam, key, ((Scalar) absSubTree).getValue());
						else
							marshalTree(listItemParam, (Tree) absSubTree);
					}
				}
			}
		}
		else if (treeList.size() == 1 && (treeList.get(0) instanceof Scalar))
		{
			marshalScalar(element, key, ((Scalar) treeList.get(0)).getValue());
		}
	}

	private void marshalScalar(Node element, String key, String value)
	{
		Document document = element.getOwnerDocument();
		Element param = createElement(document, WFSMessageConstants.PARAMETER);
		param.setAttribute(WFSMessageConstants.PARAMETER_DATA_TYPE, "String");
		param.setAttribute(WFSMessageConstants.PARAMETER_NAME, key);
		
		Element e = createElement(document, WFSMessageConstants.PARAMETER_VALUE);

		if((value != null && (value.indexOf('<') >= 0 && value.indexOf('>') >= 0)))
			e.appendChild(document.createCDATASection(value));
		else
			e.setTextContent(value);
		param.appendChild(e);

		element.appendChild(param);
	}

	/**
	 * @param document
	 * @param name
	 * @return
	 * 
	 * creates the element from the document
	 */
	private Element createElement(Document document, String name)
	{
		return document.createElement(name);
	}

	/**
	 * @param doc
	 * @param wfsMessage
	 * 
	 * marshals the source & response destination if present
	 */
	private void marshalProperties(Document doc, WorkflowMessage wfsMessage)
	{
		// create source
		Element sourceElement = this.createSourceElement(doc, wfsMessage.getSource());
		if (sourceElement != null)
			doc.getDocumentElement().appendChild(sourceElement);
		else
			logger.debug("No source element present");

		// create response destination
		Element responseDestinationElement = this.createResponseDestinationElement(doc, wfsMessage.getResponseDestination());
		
		if (responseDestinationElement != null)
			doc.getDocumentElement().appendChild(responseDestinationElement);
		else
			logger.debug("No response destination present");
	}

	/**
	 * @param doc
	 * @param source
	 * @return
	 * 
	 * creates the source element from the ISource
	 */
	public Element createSourceElement(Document doc, ISource source)
	{
		Element sourceElement = null;
		if (source != null)
		{
			Element deviceInfoElement = this.createDeviceElement(doc, source);
			
			if (deviceInfoElement != null)
			{
				sourceElement = createElement(doc, WFSMessageConstants.SOURCE);
				sourceElement.setAttribute(WFSMessageConstants.SOURCE_NAME, source.getName());
				sourceElement.appendChild(deviceInfoElement);
			}
		}

		return sourceElement;
	}

	/**
	 * @param doc
	 * @param responseDestination
	 * @return
	 * 
	 * creates the response destination element from the IDestination
	 */
	public Element createResponseDestinationElement(Document doc, IDestination responseDestination)
	{
		Element responseDestinationElement = null;
		
		if (responseDestination != null)
		{
			Element deviceInfoElement = this.createDeviceElement(doc, responseDestination);
			if (deviceInfoElement != null)
			{
				responseDestinationElement = createElement(doc, WFSMessageConstants.RESPONSE_DESTINATION);
				responseDestinationElement.appendChild(deviceInfoElement);
			}
		}
		
		return responseDestinationElement;
	}

	/**
	 * @param doc
	 * @param device
	 * @return
	 * 
	 * creates the device element by checking for the right type (http or tcp for now)
	 */
	private Element createDeviceElement(Document doc, IDevice device)
	{
		Element deviceInfoElement = null;

		if (device instanceof HTTPDevice)
		{
			Element sourceUrlElement = createElement(doc, WFSMessageConstants.URL);
			String url = ((HTTPDevice) device).getUrl();
			sourceUrlElement.setTextContent(url);
			deviceInfoElement = sourceUrlElement;
		}
		else if (device instanceof TCPDevice)
		{
			Element sourceHostElement = createElement(doc, WFSMessageConstants.HOST);
			String ip = ((TCPDevice) device).getIP();
			int port = (((TCPDevice) device).getPort());
			sourceHostElement.setAttribute(WFSMessageConstants.HOST_IP, ip);
			sourceHostElement.setAttribute(WFSMessageConstants.HOST_PORT, Integer.toString(port));
			deviceInfoElement = sourceHostElement;
		}

		return deviceInfoElement;
	}
	
	/**
	 * Creates an element representing the WPCL command
	 * 
	 * @param document
	 */
	private void marshalCommand(WPCLCommand command, Document document)
	{
		Element e = document.createElement(WFSMessageConstants.COMMAND);
		e.setAttribute("Name", command.getName());
		
		for (String key : command.getParameters().keySet())
		{
			Element parameter = document.createElement(WFSMessageConstants.COMMAND_PARAMETER);
			
			parameter.setAttribute(WFSMessageConstants.PARAMETER_NAME, key);
			parameter.setAttribute(WFSMessageConstants.PARAMETER_VALUE, command.getParameterValue(key));
			e.appendChild(parameter);
		}
		
		document.getDocumentElement().appendChild(e);
	}
}
