package com.tandbergtv.metadatamanager.validation.Schematron;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamSource;

import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.sun.org.apache.xml.internal.serialize.OutputFormat;
import com.sun.org.apache.xml.internal.serialize.XMLSerializer;
import com.tandbergtv.metadatamanager.spec.IValidator;
import com.tandbergtv.metadatamanager.util.ResourceResolver;
import com.tandbergtv.metadatamanager.validation.ValidationError;

/**
 * Validator that validates a document as per the rules set in the rules file. A
 * Schematron rules file is passed to this class which is first converted to a
 * xsl by running 3 conversion steps. The files are saved in the temp folder.
 * Every time this validator is instantiated it cleans up any existing files
 * that it previously wrote.
 * 
 * The input doc is always validated against this converted xsl. The report is
 * parsed to create the required error objects.
 * 
 * Note: Saxon could have been used to do this conversion in one step but had
 * issues with file paths when deployed as a web app. If some one figures out a
 * way to use it, the initial conversion may turn out to be much simpler.
 * 
 * @author spuranik
 * 
 */
public class SchematronValidator implements IValidator {

	protected final Logger logger = Logger.getLogger(this.getClass());

	/* name for this validator instance */
	private String name;

	/* file which holds the translated rules. */
	private File convertedRules;
	/* converted rules name into xsl by schematron */
	private String CONVERTED_RULES_XSL = "convertedRules.xsl";

	/* xsl used by schematron to convert rules into xsl for its operation. */
	private static String SCHEMATRON_DSDL_INCLUDE_XSL = "iso_dsdl_include.xsl";
	private static String SCHEMATRON_EXPAND_XSL = "iso_abstract_expand.xsl";
	private static String SCHEMATRON_CONVERSION_XSL = "iso_svrl.xsl";

	/* Schematron report xml elements */
	private static String SVRL_URI = "http://purl.oclc.org/dsdl/svrl";
	private static String REPORT = "successful-report";
	private static String LOCATION_ATTR = "location";
	private static String FAILED_ASSERT_TEXT = "text";

	/* attribute location includes '@iso:schema/'. Have to remove that */
	private static String ATTR_XPATH = "/@iso:schema";
	private static String REPLACE_ATTR_XPATH = "";

	/* used to build temp conversion file names */
	private static String TEMP_FILE_DELIMITER = "_";

	private ClassLoader classLoader;
	/**
	 * @return the name
	 */
	public String getName() {
		return name;
	}

	public SchematronValidator(String name, String rulesResource) {
		InputStream is = this.getClass().getClassLoader().getResourceAsStream(
				rulesResource);
		init(name, is);
	}

	public SchematronValidator(String name, InputStream is) {
		init(name, is);
	}

	public SchematronValidator(String name, InputStream is, ClassLoader loader) {
		this.classLoader = loader;
		init(name, is);
	}

	private void init(String name, InputStream is) {
		this.name = name;

		// step1 of converting the rules sch file to a xsl
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		DocumentBuilder builder;
		Document rulesDoc;
		try {
			builder = factory.newDocumentBuilder();
			rulesDoc = builder.parse(is);
		} catch (ParserConfigurationException e) {
			throw new RuntimeException(e);
		} catch (SAXException e) {
			throw new RuntimeException(e);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
		Document temp1 = transform(classLoader.getResourceAsStream(
				SCHEMATRON_DSDL_INCLUDE_XSL), rulesDoc);

		// step2 of converting the rules sch file to a xsl
		Document temp2 = transform(classLoader.getResourceAsStream(
				SCHEMATRON_EXPAND_XSL), temp1);

		// step3 of converting the rules sch file to a xsl
		Document temp3 = transform(classLoader.getResourceAsStream(
				SCHEMATRON_CONVERSION_XSL), temp2);
		try {
			convertedRules = File.createTempFile(name + TEMP_FILE_DELIMITER,
					CONVERTED_RULES_XSL);
			cleanup(convertedRules.getParent());
			save(temp3, convertedRules);

		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Deletes any previously created converted file by this validator.
	 * 
	 * @param path
	 */
	private void cleanup(String path) {
		File dir = new File(path);
		FilenameFilter filter = new FilenameFilter() {
			public boolean accept(File dir, String name) {
				// it only gets the file name starting with the validator
				// name
				return name.contains(getName() + TEMP_FILE_DELIMITER);
			}
		};
		String[] files = dir.list(filter);
		for (String filename : files) {
			File f = new File(path, filename);
			f.delete();
			logger.debug("Deleted previously created file: " + path + filename);
		}
	}

	/**
	 * Transforms the given document as per the xsl in the stream provided.
	 * 
	 * @param stream
	 * @param input
	 * @return
	 */
	private Document transform(InputStream stream, Document input) {
		
        TransformerFactory tf = TransformerFactory.newInstance(
                "net.sf.saxon.TransformerFactoryImpl",
                classLoader);
        if(classLoader == null) {
    		tf.setURIResolver(new ResourceResolver());
        } else {
    		tf.setURIResolver(new ResourceResolver(classLoader));
        }

        Transformer transformer = null;
		try {
			transformer = tf.newTransformer(new StreamSource(stream));
		} catch (TransformerConfigurationException e) {
			throw new RuntimeException(e);
		}
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		DocumentBuilder builder;
		try {
			builder = factory.newDocumentBuilder();
			Document docOut = builder.newDocument();
			DOMResult result = new DOMResult(docOut);
			transformer.transform(new DOMSource(input), result);
			return docOut;
		} catch (ParserConfigurationException e) {
			throw new RuntimeException(e);
		} catch (TransformerException e) {
			throw new RuntimeException(e);
		}
	}

	public List<ValidationError> validate(Document doc) {
		FileInputStream stream;
		try {
			stream = new FileInputStream(convertedRules);
		} catch (FileNotFoundException e1) {
			throw new RuntimeException(e1);
		}
		Document report = transform(stream, doc);
		return parse(report);
	}

	/**
	 * Parses the schematron report document and prepares a list of error
	 * objects.
	 * 
	 * @param reportDoc
	 * @return
	 */
	private List<ValidationError> parse(Document reportDoc) {
		List<ValidationError> errors = new ArrayList<ValidationError>();
		NodeList asserts = reportDoc.getElementsByTagNameNS(SVRL_URI, REPORT);
		for (int i = 0; i < asserts.getLength(); i++) {
			Element node = (Element) asserts.item(i);
			String location = node.getAttribute(LOCATION_ATTR);
			if (location.contains(ATTR_XPATH)) {
				location = location.replace(ATTR_XPATH, REPLACE_ATTR_XPATH);
			}
			String text = node.getElementsByTagNameNS(SVRL_URI,
					FAILED_ASSERT_TEXT).item(0).getTextContent();

			// split the fields and info which is of the form error code :
			// field1, field2,... : info1, info2
			String[] errorText = text.split(Constants.ERROR_TEXT_DELIMITER);
			String errorCode = new String();
			List<String> errorFields = new ArrayList<String>();
			List<String> errorInfo = new ArrayList<String>();
			if (errorText.length > 0) {
				// error code
				errorCode = errorText[Constants.ERROR_CODE_POSITION];
				// fields
				String[] errorFieldList = errorText[Constants.ERROR_FIELDS_POSITION]
						.split(Constants.INFO_DELIMITER);
				for (String field : errorFieldList) {
					errorFields.add(field.trim());
				}
				// error info is optional
				if (errorText.length == Constants.ERROR_MAX_ITEM_COUNT) {
					// info
					String[] errorInfoList = errorText[Constants.ERROR_INFO_POSITION]
							.split(Constants.INFO_DELIMITER);
					for (String field : errorInfoList) {
						errorInfo.add(field.trim());
					}
				}
			}

			ValidationError error = new ValidationError(errorCode, location,
					errorFields, errorInfo);
			// do not add duplicate errors.
			if (!isDuplicate(errors, error)) {
				errors.add(error);
			}
		}
		return errors;
	}

	/**
	 * Checks if the list contains the error passed in
	 * 
	 * @param errors
	 * @param error
	 * @return
	 */
	private boolean isDuplicate(List<ValidationError> errors,
			ValidationError error) {
		for (ValidationError e : errors) {
			// if the location, error code and fields match it already exists in
			// the list
			if (e.getErrorLocation().equalsIgnoreCase(error.getErrorLocation())
					&& e.getErrorCode().equalsIgnoreCase(error.getErrorCode())
					&& e.getErrorFields().size() == error.getErrorFields().size()) {
				for (String fieldName : e.getErrorFields()) {
					if(!error.getErrorFields().contains(fieldName))
						return false;
				}
				return true;
			}
		}
		return false;
	}

	/**
	 * Writes the xml document to the given file.
	 * 
	 * @param doc
	 * @param outFile
	 */
	private void save(Document doc, File outFile) {
		try {
			OutputFormat format = new OutputFormat(doc);
			format.setIndenting(true);
			XMLSerializer serializer = new XMLSerializer(new FileOutputStream(
					outFile, false), format);
			serializer.serialize(doc);
		} catch (FileNotFoundException e) {
			throw new RuntimeException(e);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

}
