package com.tandbergtv.metadatamanager.util;

import java.io.InputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Result;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.sun.org.apache.xpath.internal.XPathAPI;
import com.tandbergtv.metadatamanager.model.CustomField;
import com.tandbergtv.metadatamanager.model.Field;

/**
 * FieldBinder - performs bidirectional converts between the Fields TTV xml
 * element and Asset model's representation of fields which is a list of indexed
 * xpaths plus values.
 * 
 * @author nicholas
 * 
 */
public class FieldBinder {

	public static final String _CUSTOMFIELD_NAME = "tns:CustomField[@name=";
	private DocumentBuilder builder;
	private Transformer transformer;
	private static final String FIELDS = "tns:Fields", XMLNS_TNS_NAME = "xmlns:tns",
			XMLNS_TNS_VALUE = "http://www.tandbergtv.com/TTVSchema", XMLNS_XSI_NAME = "xmlns:xsi",
			XMLNS_XSI_VALUE = "http://www.w3.org/2000/XMLSchema-instance",
			XSI_SCHEMA_LOCATION_NAME = "xsi:schemaLocation",
			XSI_SCHEMA_LOCATION_VALUE = "http://www.tandbergtv.com/TTVSchema TTVSpec.xsd",
			XSLT = "com/tandbergtv/metadatamanager/util/xpath.xsl";

	public FieldBinder() {

		try {
			builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
		} catch (ParserConfigurationException e) {
			builder = null;
		}

		InputStream is = FieldBinder.class.getClassLoader().getResourceAsStream(XSLT);
		try {
			TransformerFactory tf = TransformerFactory.newInstance("net.sf.saxon.TransformerFactoryImpl",
					FieldBinder.class.getClassLoader());

			tf.setURIResolver(new ResourceResolver());
			transformer = tf.newTransformer(new StreamSource(is));
		} catch (TransformerConfigurationException e) {
			throw new RuntimeException(e);
		} catch (TransformerFactoryConfigurationError e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Converts a list of Fields, which consist of xpath value pairs, into a
	 * tns:Fields xml document to be included in the xml of an asset.
	 * 
	 * Note: Assumes that customFields are simple types and does not deal with
	 * their indices at all.
	 * 
	 * @param fields
	 *            the list of Fields to be converted
	 * @return Document containing the ttv Fields element
	 */
	public Document buildXml(List<Field> fields) {

		Document doc = builder.newDocument();

		Element root = doc.createElementNS(XMLNS_TNS_VALUE, FIELDS);
		root.setAttribute(XMLConstants.XMLNS_ATTRIBUTE, XMLConstants.XMLNS_ATTRIBUTE_NS_URI);
		root.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, XMLNS_XSI_NAME, XMLNS_XSI_VALUE);
		root.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, XMLNS_TNS_NAME, XMLNS_TNS_VALUE);
		root.setAttributeNS(XMLNS_XSI_VALUE, XSI_SCHEMA_LOCATION_NAME, XSI_SCHEMA_LOCATION_VALUE);
		doc.appendChild(root);

		for (Field field : fields) {
			String xpath = field.getTtvXPath();
			String attribute = "";
			// pull out the attribute from the xpath if it exists
			if (xpath.indexOf("/@") > -1) {
				attribute = xpath.replaceFirst(".*/@", "");
				xpath = xpath.replaceFirst("/@.*", "");
			}
			String[] xpathSplit = xpath.split(CustomField.XPATH_DELIMITER);
			List<Integer> indices = field.getIndices();
			String value = field.getValue();

			String xpathPart = "";
			Node node = null;
			// for each level in the xpath
			for (int i = 0; i < indices.size(); i++) {
				String xpathChild = xpathSplit[i + 1];
				int index = indices.get(i);
				try {
					if (xpathChild.contains(_CUSTOMFIELD_NAME)) {
						node = XPathAPI.selectSingleNode(root, xpathPart, root);
						node = node.appendChild(doc.createElementNS(XMLNS_TNS_VALUE, CustomField.CUSTOMFIELD));
						String cfName = extractCustomFieldName(xpathChild);
						((Element) node).setAttributeNS(XMLNS_TNS_VALUE, CustomField.NAME, cfName);
					} else {
						NodeList nl = XPathAPI.selectNodeList(root, xpathPart + CustomField.XPATH_DELIMITER + xpathChild, root);
						int numNodes = nl.getLength();
						// element already exists
						if (numNodes >= index) {
							node = nl.item(index - 1);
							// one or more elements need to be created
						} else {
							for (int j = 0; j < index - numNodes; j++) {
								Node parent = XPathAPI.selectSingleNode(root, xpathPart, root);
								node = parent.appendChild(doc.createElementNS(XMLNS_TNS_VALUE, xpathChild));
							}
						}
					}
				} catch (TransformerException e) {
					e.printStackTrace();
				}
				xpathPart += CustomField.XPATH_DELIMITER + xpathChild + "[" + index + "]";
			}
			if (!attribute.equals("")) {
				((Element) node).setAttributeNS(XMLNS_TNS_VALUE, attribute, value);
			} else {
				((Element) node).setTextContent(value);
			}
		}
		return doc;
	}

	/**
	 * Converts a Document containing the root tns:Fields into a list of Fields
	 * to be added to an Asset model
	 * 
	 * Note: Assumes that customFields are simple types and does not deal with
	 * their indices at all.
	 * 
	 * @param doc
	 *            containing the tns:Fields element to be converted into fields
	 *            in the asset model
	 * @return List<Field> the list of Fields
	 */
	public List<Field> extractXPaths(Document doc) {
		List<Field> fields = new ArrayList<Field>();
		StringWriter sw = new StringWriter();
		Result result = new StreamResult(sw);
		try {
			transformer.transform(new DOMSource(doc), result);
		} catch (TransformerException e) {
			throw new RuntimeException(e);
		}
		// TODO: This is a hack. Need to implement something to escape the
		// newline in the incoming text in the above transform
		String[] xpaths = sw.toString().split("#ttv#ttv#ttv#ttv#\n");

		// CustomFieldHandling
		xpaths = handleCustomFieldXpaths(xpaths);
		for (String xpath : xpaths) {
			// ignore tns:Fields attributes
			if (xpath.matches("/tns:Fields\\{[0-9]*\\}/@.*"))
				continue;

			Field field = new Field();

			int equalSign = xpath.indexOf("==");
			String indexedXPath = xpath.substring(0, equalSign);

			field.setTtvXPath(indexedXPath.replaceAll("\\{[0-9]*\\}", ""));
			field.setValue(xpath.substring(equalSign + 2));

			// extract indices from the xpath and add them to the field
			List<Integer> indices = new ArrayList<Integer>();
			String[] indexSplit = indexedXPath.split("\\{");
			for (String indexStr : indexSplit) {
				int bracket = indexStr.indexOf("}");
				if (bracket > -1)
					indices.add(Integer.parseInt(indexStr.substring(0, bracket)));
			}
			field.setIndices(indices);

			fields.add(field);
		}
		return fields;
	}

	private String[] handleCustomFieldXpaths(String[] xpaths) {
		ArrayList<String> list = new ArrayList<String>();
		ArrayList<String> cfList = new ArrayList<String>();

		// first add all xpaths to the return list that do not have anything to
		// do with customFields. Add the ones that are for customFields to the
		// cflist
		for (String xpath : xpaths) {
			if (xpath.contains(CustomField.TNS_CUSTOMFIELDS)) {
				cfList.add(xpath);
			} else {
				list.add(xpath);
			}
		}

		Collection<CustomField> customFields = getCustomFields(cfList);

		for (CustomField customField: customFields) { 
			list.add(customField.constructTTVString());
		}

		return list.toArray(new String[0]);
	}

	/**
	 * 
	 * @param customFieldsXPaths
	 * @return 
	 */
	public Collection<CustomField> getCustomFields(List<String> customFieldsXPaths) {
		// this is a sample of entries in cfList
		// tns:Fields{1}/tns:CustomFields{1}/tns:CustomField{1}/@name==CF1
		// tns:Fields{1}/tns:CustomFields{1}/tns:CustomField{1}/@value==val1
		// tns:Fields{1}/tns:CustomFields{1}/tns:CustomField{1}==
		// tns:Fields{1}/tns:CustomFields{1}/tns:CustomField{2}/@name==CF2
		// tns:Fields{1}/tns:CustomFields{1}/tns:CustomField{2}/@value==val2
		// tns:Fields{1}/tns:CustomFields{1}/tns:CustomField{2}==

		// next for loop will produce a list that has the following contents:
		// CF1, val1, CF2, val2
		Map<String, CustomField> customFields = new HashMap<String, CustomField>();

		for (String xpath : customFieldsXPaths) {
			if (!xpath.contains(CustomField.CUSTOM_FIELD_XPATH_DISTINGUISHER)) {
				continue;
			}
			String[] xpathSplit = xpath.split("==");
			if (xpathSplit.length != 2) {
				continue;
			} else {
				if (xpathSplit[0].endsWith(CustomField.XPATH_DELIMITER + CustomField.ATTRIBUTE_VALUE)) {
					CustomField customField = getCustomField(customFields, xpathSplit[0], CustomField.ATTRIBUTE_VALUE);
					customField.setValue(xpathSplit[1]);
				} else {
					CustomField customField = getCustomField(customFields, xpathSplit[0], CustomField.ATTRIBUTE_NAME);
					customField.setName(xpathSplit[1]);
				}
			}
		}
		return customFields.values();
	}

	/**
	* For xpath like "tns:Fields{1}/tns:CustomFields{1}/tns:CustomField{1}/@name==CF1", get its corresponding CustomField
	 * whose xpath field is "tns:Fields{1}/tns:CustomFields{1}/tns:CustomField{1}" (i.e. input param xpath without the ending attribute)
	 * 
	 * @param customFields
	 * @param xPath
	 * @param attributeKey
	 * @return
	 */
	private CustomField getCustomField(Map<String, CustomField> customFields, String xPath, String attributeKey) {
		String xPathWithoutAttribute = xPath.substring(0, xPath.lastIndexOf(CustomField.XPATH_DELIMITER + attributeKey));
		CustomField customField = customFields.get(xPathWithoutAttribute);
		if (customField == null) {
			customField = new CustomField(xPathWithoutAttribute);
			customFields.put(xPathWithoutAttribute, customField);
		}
		return customField;
	}

	private String extractCustomFieldName(String xpathChild) {
		String cfName = "";
		String[] o = xpathChild.split("=");
		if (o.length == 2) {
			cfName = o[1];
			cfName = cfName.replaceAll("\\]", "");
		}
		return cfName;
	}
}
