package org.mustangproject;

import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;

import org.apache.commons.io.IOUtils;
import org.dom4j.io.XMLWriter;
import org.mustangproject.ZUGFeRD.ZUGFeRDDateFormat;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

public class XMLTools extends XMLWriter {
	@Override
	public String escapeAttributeEntities(String s) {
		return super.escapeAttributeEntities(s);
	}

	@Override
	public String escapeElementEntities(String s) {
		return super.escapeElementEntities(s);
	}

	public static DocumentBuilder getDocumentBuilder(boolean namespaceAware) throws ParserConfigurationException {
		final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		//REDHAT
		//https://www.blackhat.com/docs/us-15/materials/us-15-Wang-FileCry-The-New-Age-Of-XXE-java-wp.pdf
		try {
			dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
		} catch (ParserConfigurationException e) {
			// ignore
		}
		try {
			dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
		} catch (IllegalArgumentException e) {
			// ignore
		}
		try {
			dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
		} catch (IllegalArgumentException e) {
			// ignore
		}

		//OWASP
		//https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
		try {
			dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
		} catch (ParserConfigurationException e) {
			// ignore
		}
		try {
			dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
		} catch (ParserConfigurationException e) {
			// ignore
		}
		try {
			dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
		} catch (ParserConfigurationException e) {
			// ignore
		}
		try {
		// Disable external DTDs as well
			dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
		} catch (ParserConfigurationException e) {
			// ignore
		}
		// and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks"
		dbf.setXIncludeAware(false);
		dbf.setExpandEntityReferences(false);
		dbf.setNamespaceAware(namespaceAware);
		return dbf.newDocumentBuilder();
	}

	public static Validator getValidator(URL schemaFile) throws SAXException {
		SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
		try {
			schemaFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
		} catch (SAXNotSupportedException | SAXNotRecognizedException e) {
			// ignore
		}
		Schema schema = schemaFactory.newSchema(schemaFile);

		Validator validator = schema.newValidator();
		try {
			validator.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
		} catch (SAXNotSupportedException | SAXNotRecognizedException e) {
			// ignore
		}
		return validator;
	}

	public static TransformerFactory getTransformerFactory() {
		TransformerFactory factory = new net.sf.saxon.TransformerFactoryImpl();
		try {
			factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
		} catch (TransformerConfigurationException e) {
			// ignore
		}
		return factory;
	}

	public static String nDigitFormat(BigDecimal value, int scale) {
		/*
		 * I needed 123.45, locale independent.I tried
		 * NumberFormat.getCurrencyInstance().format( 12345.6789 ); but that is locale
		 * specific.I also tried DecimalFormat df = new DecimalFormat( "0,00" );
		 * df.setDecimalSeparatorAlwaysShown(true); df.setGroupingUsed(false);
		 * DecimalFormatSymbols symbols = new DecimalFormatSymbols();
		 * symbols.setDecimalSeparator(','); symbols.setGroupingSeparator(' ');
		 * df.setDecimalFormatSymbols(symbols);
		 *
		 * but that would not switch off grouping. Although I liked very much the
		 * (incomplete) "BNF diagram" in
		 * http://docs.oracle.com/javase/tutorial/i18n/format/decimalFormat.html in the
		 * end I decided to calculate myself and take eur+sparator+cents
		 *
		 */
		return value.setScale(scale, RoundingMode.HALF_UP).toPlainString();

	}

	/**
	 * returns the value of an node
	 *
	 * @param node the Node to get the value from
	 * @return A String or empty String, if no value was found
	 */
	public static String getNodeValue(Node node) {
		if (node != null && node.getFirstChild() != null) {
			return node.getFirstChild().getNodeValue();
		}
		return "";
	}


	/**
	 * tries to convert a String to BigDecimal.
	 *
	 * @param nodeValue The value as String
	 * @return a BigDecimal with the value provides as String or a BigDecimal with value 0.00 if an error occurs
	 */
	public static BigDecimal tryBigDecimal(String nodeValue) {
		try {
			return new BigDecimal(nodeValue);
		} catch (final Exception e) {
			try {
				return BigDecimal.valueOf(Float.valueOf(nodeValue));
			} catch (final Exception ex) {
				return new BigDecimal("0.00");
			}
		}
	}


	/**
	 * tries to convert a Node to a BigDecimal.
	 *
	 * @param node The value as String
	 * @return a BigDecimal with the value provides as String or a BigDecimal with value 0.00 if an error occurs
	 */
	public static BigDecimal tryBigDecimal(Node node) {
		final String nodeValue = XMLTools.getNodeValue(node);
		if (nodeValue.isEmpty()) {
			return null;
		}
		return XMLTools.tryBigDecimal(nodeValue);
	}
	/***
	 * formats a number so that at least minDecimals are displayed but at the maximum maxDecimals are there, i.e.
	 * cuts potential 0s off the end until minDecimals
	 * @param value the value to be formatted
	 * @param maxDecimals number of maximal scale
	 * @param minDecimals number of minimal scale
	 * @return value as String with decimals in the specified range
	 */
	public static String nDigitFormatDecimalRange(BigDecimal value, int maxDecimals, int minDecimals) {
		if (maxDecimals < minDecimals || maxDecimals < 0 || minDecimals < 0) {
			throw new IllegalArgumentException("Invalid scale range provided");
		}
		int curDecimals = maxDecimals;
		while (curDecimals > minDecimals && value.setScale(curDecimals, RoundingMode.HALF_UP).compareTo(value.setScale(curDecimals - 1, RoundingMode.HALF_UP)) == 0) {
			 curDecimals--;
		}
		return value.setScale(curDecimals, RoundingMode.HALF_UP).toPlainString();

	}


	/***
	 * returns a util.Date from a 102 String yyyymmdd in a node
	 * @param node the node
	 * @return a util.Date, or null, if not parseable
	 */
	public static Date tryDate(Node node) {
		final String nodeValue = XMLTools.getNodeValue(node);
		if (nodeValue.isEmpty()) {
			return null;
		}
		return tryDate(nodeValue);
	}

	/***
	 * returns a util.Date from a 102 String yyyymmdd
	 * @param toParse the string
	 * @return a util.Date, or null, if not parseable
	 */
	public static Date tryDate(String toParse) {
		SimpleDateFormat formatter = null;
		if (toParse == null) {
			return null;
		}
		if (toParse.contains("-")) {
			// from ubl
			 formatter = new SimpleDateFormat("yyyy-MM-dd");
		} else {
			formatter = ZUGFeRDDateFormat.DATE.getFormatter();
		}
		try {
			return formatter.parse(toParse);
		} catch (final Exception e) {
			return null;
		}
	}

	/***
	 * relplaces some entities like &lt; , &gt; and &amp; with their escaped pendant like &amp;lt;
	 * @param s the string
	 * @return the "safe" string
	 */
	public static String encodeXML(CharSequence s) {
		if (s == null) {
			return "";
		}
		final StringBuilder sb = new StringBuilder();
		final int len = s.length();
		for (int i = 0; i < len; i++) {
			int c = s.charAt(i);
			if (c >= 0xd800 && c <= 0xdbff && i + 1 < len) {
				c = ((c - 0xd7c0) << 10) | (s.charAt(++i) & 0x3ff);    // UTF16 decode
			}
			if (c < 0x80) { // ASCII range: test most common case first
				if (c < 0x20 && c != '\t' && c != '\r' && c != '\n') { // Illegal XML character, even encoded. Skip or substitute
					sb.append("&#xfffd;"); // Unicode replacement character
				} else {
					switch (c) {
						case '&':
							sb.append("&amp;");
							break;
						case '>':
							sb.append("&gt;");
							break;
						case '<':
							sb.append("&lt;");
							break;
						// Uncomment next two if encoding for an XML attribute
//	                  case '\''  sb.append("&apos;"); break;
//	                  case '\"'  sb.append("&quot;"); break;
						// Uncomment next three if you prefer, but not required
//	                  case '\n'  sb.append("&#10;"); break;
//	                  case '\r'  sb.append("&#13;"); break;
//	                  case '\t'  sb.append("&#9;"); break;

						default:
							sb.append((char) c);
					}
				}
			} else if (c >= 0xd800 && c <= 0xdfff || c == 0xfffe || c == 0xffff) {
				// Illegal XML character, even encoded. Skip or substitute
				sb.append("&#xfffd;");   // Unicode replacement character
			} else {
				sb.append("&#x");
				sb.append(Integer.toHexString(c));
				sb.append(';');
			}
		}
		return sb.toString();
	}

	/***
	 * removes utf8 byte order marks from byte arrays, in case one is there
	 * @param zugferdRaw the CII XML
	 * @return the byte array without bom
	 */
	public static byte[] removeBOM(byte[] zugferdRaw) {
		final byte[] zugferdData;
		// This handles the UTF-8 BOM
		if (zugferdRaw[0] == (byte) 0xEF && zugferdRaw[1] == (byte) 0xBB && zugferdRaw[2] == (byte) 0xBF) {
			// I don't like BOMs, lets remove it
			zugferdData = new byte[zugferdRaw.length - 3];
			System.arraycopy(zugferdRaw, 3, zugferdData, 0, zugferdRaw.length - 3);
		} else {
			zugferdData = zugferdRaw;
		}
		return zugferdData;
	}

	public static byte[] getBytesFromStream(InputStream fileinput) throws IOException {
		// Stream closing responsibility is with the caller
		return IOUtils.toByteArray(fileinput);
	}


	public static String trimOrNull(Node node) {
		if (node != null) {
			String textContent = node.getTextContent();
			if (textContent != null) {
				return textContent.trim();
			}
		}
		return null;
	}


}
