package org.etsi.mts.ttcn.part9.xmldiff;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.custommonkey.xmlunit.Difference;
import org.custommonkey.xmlunit.DifferenceEngine;
import org.custommonkey.xmlunit.DifferenceListener;
import org.custommonkey.xmlunit.NodeDetail;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.TypeInfo;

public class DifferenceHandler implements DifferenceListener {
	private static final String DOUBLE_TYPE_NAME = "double";
	private static final String FLOAT_TYPE_NAME = "float";
	private Document originalReference;
	@SuppressWarnings("unused")
	private Document originalTest;
	private XPathFactory xpathFactory;

	public DifferenceHandler(Document originalReference, Document originalTest) {
		super();
		this.originalReference = originalReference;
		this.originalTest = originalTest;
		xpathFactory = XPathFactory.newInstance();
	}


	//@Override
    public int differenceFound(Difference difference) {
    	int id = difference.getId();
		if (id == DifferenceEngine.NAMESPACE_PREFIX_ID) {
			return handlePrefixDiff(difference);
    	}
		if (id == DifferenceEngine.TEXT_VALUE_ID) {
			return handleValueDiff(difference);
		}
		if (id == DifferenceEngine.ATTR_VALUE_ID) {
			return handleValueDiff(difference);			
		}
		XmlDiff.logger.debug2("Difference accepted: Namespace prefix ID", 
				difference.getDescription());
    	return RETURN_ACCEPT_DIFFERENCE;
    }


	private int handleValueDiff(Difference difference) {
		
		NodeDetail controlNodeDetail = difference.getControlNodeDetail();
		NodeDetail testNodeDetail = difference.getTestNodeDetail();
		
		// XmlDiff clones DOM trees and loses information about type of nodes and attributes
		// We need to find corresponding node in the original tree to get information about 
		// schema type
		Node originalNode = findOriginalNode(controlNodeDetail);
		
		if (originalNode == null) {
			return RETURN_ACCEPT_DIFFERENCE;
		}
		
		if (isFloat(originalNode) || isDouble(originalNode)) {
			return handleFloat(controlNodeDetail, testNodeDetail);
		}
		return RETURN_ACCEPT_DIFFERENCE;
	}

	private int handleFloat(NodeDetail controlNodeDetail,
			NodeDetail testNodeDetail) {
		String controlValue = controlNodeDetail.getValue();
		double controlDouble = 0.0;
		
		try {
			controlDouble = Double.parseDouble(controlValue);
		} catch (NumberFormatException err) {
			XmlDiff.logger.severe("Failed to convert reference value to double {", controlValue, "}: error ",
					err.getMessage());
			return RETURN_ACCEPT_DIFFERENCE;
		}

		String testValue = testNodeDetail.getValue();
		double testDouble = 0.0;
		try {
			testDouble = Double.parseDouble(testValue);
		} catch (NumberFormatException err) {
			XmlDiff.logger.severe("Failed to convert test value to double {", testValue, "}: error ",
					err.getMessage());
			return RETURN_ACCEPT_DIFFERENCE;
		}
		
		if (testDouble == controlDouble) {
			XmlDiff.logger.debug("Compare {", controlValue, "} and {", testValue, "}: identical");
			return RETURN_IGNORE_DIFFERENCE_NODES_IDENTICAL;
		} else {
			XmlDiff.logger.debug("Compare {", controlValue, "} and {", testValue, "}: different");
			return RETURN_ACCEPT_DIFFERENCE;
		}
	}


	private Node findOriginalNode(NodeDetail nodeDetail) {
		String controlXpath = fixXpath(nodeDetail.getXpathLocation());
		XPath xpath = xpathFactory.newXPath();
		Node result = null;
		try {
			result = (Node) xpath.evaluate(controlXpath, 
					originalReference, 
					XPathConstants.NODE);
		} catch (XPathExpressionException e) {
			XmlDiff.logger.severe("Failed to process XPATH expression '", 
					controlXpath, "': error " + e.getMessage());
			return null;
		}
		if (result == null) {
			XmlDiff.logger.severe("Nothing found by XPATH expression '", 
					controlXpath, "'");
		}
		return result;
	}


	private boolean isFloat(Node node) {
		return isDerivedFrom(node, FLOAT_TYPE_NAME);
	}

	private boolean isDouble(Node node) {
		return isDerivedFrom(node, DOUBLE_TYPE_NAME);
	}


	private boolean isDerivedFrom(Node node, String typeName) {
		boolean result = false;
		if (node.getNodeType() == Node.TEXT_NODE) {
			node  = node.getParentNode();
		}
		TypeInfo typeinfo = guessTypeInfo(node);
		if (typeinfo == null) {
			XmlDiff.logger.info("No type info found: node ", node.getNodeName());
			return false;
		}
		
		result = typeinfo.isDerivedFrom(
							"http://www.w3.org/2001/XMLSchema", 
							typeName, 
							TypeInfo.DERIVATION_RESTRICTION)
			|| typeinfo.isDerivedFrom(
					"http://www.w3.org/2001/XMLSchema", 
					typeName, 
					TypeInfo.DERIVATION_EXTENSION);
		XmlDiff.logger.debug2("isDerivedFrom(", typeName, ") == ", result, ": type ", typeinfo.getTypeName(), 
				", node ", node.getNodeName());
		return result;
	}


	private int handlePrefixDiff(Difference difference) {
		XmlDiff.logger.debug3("Difference ignored: Namespace prefix ID", 
				difference.getDescription());
		return RETURN_IGNORE_DIFFERENCE_NODES_IDENTICAL;
	}
    

	//@Override
	public void skippedComparison(Node control, Node test) {
	}
	// Transform XPath from /nodename[1]/ to /[local-name()='nodename'/
	// The reason is that the order of nodes is not strictly defined in an XML document due to . 
	private static String fixXpath(String orig) {
		// This regular expression is invalid: it fails on names like ns1:MyType日本
		// String result = orig.replaceAll("/([a-zA-Z0-9_-]+)(\\[[0-9]+\\](/|$))?", "/*[local-name()='$1']$2");
		// Simplified regex: (any character distinct from [)+ 
		String result = orig.replaceAll("/([^\\[/]+)(\\[[0-9]+\\](/|$))?", "/*[local-name()='$1']$2");
		XmlDiff.logger.debug2("Original xpath: {", orig, "}, modified: {", result, "}");
		return result;
	}
	
	private static TypeInfo guessTypeInfo(Node node) {
		TypeInfo typeinfo = null;
		if (node.getNodeType() == Node.ELEMENT_NODE) {
			Element element = (Element)node;
			
			typeinfo = element.getSchemaTypeInfo();
		}
		if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
			Attr attr = (Attr)node;
			
			typeinfo = attr.getSchemaTypeInfo();
		}
		
		return typeinfo;
	}
}