package eu.dnetlib.miscutils.functional.xml;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Map;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.URIResolver;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

import eu.dnetlib.miscutils.functional.UnaryFunction;

/**
 * This function applies a stylesheet to something which can be transformed to a javax.xml.Source.
 * 
 * <p>Subclasses are specialized and know how to transform K into a Source</p>
 * 
 * @author marko
 *
 * @param <K>
 */
public abstract class AbstractApplyXslt<K> implements UnaryFunction<String, K> {
	private static final String UNKNOWN_XSLT_NAME = "unknown xslt name";

	private static final Log log = LogFactory.getLog(AbstractApplyXslt.class); // NOPMD by marko on 11/24/08 5:02 PM

	private Transformer transformer;
	
	/**
	 * optional, useful to keep track of xslt name for debugging purposes.
	 */
	private String xsltName;

	public AbstractApplyXslt(final Resource xslt) {
		this(xslt, null);
	}

	public AbstractApplyXslt(final Resource xslt, Map<String, String> parameters) {
		this(new StreamSource(getInputStream(xslt)), getFileName((ClassPathResource) xslt), parameters);
	}
	
	public AbstractApplyXslt(final String xslt) {
		this(xslt, UNKNOWN_XSLT_NAME);
	}

	public AbstractApplyXslt(final String xslt, String name) {
		this(xslt, name, null);
	}

	public AbstractApplyXslt(final String xslt, String name, Map<String, String> parameters) {
		this(new StreamSource(new StringReader(xslt)), name, parameters);
	}

	public AbstractApplyXslt(final Source xslt) {
		this(xslt, UNKNOWN_XSLT_NAME);
	}

	public AbstractApplyXslt(final Source xslt, String name) {
		this(xslt, name, null);
	}

	/**
	 * Base method for all the others.
	 * @param xslt
	 * @param name
	 * @param parameters
	 */
	public AbstractApplyXslt(final Source xslt, String name, Map<String, String> parameters) {
		try {
			this.xsltName = name;
			TransformerFactory factory = TransformerFactory.newInstance();
			if (! UNKNOWN_XSLT_NAME.equals(name))
				factory.setURIResolver(new ExternalResourceURIResolver(name.replaceFirst("[^/]+$", "")));
			transformer = factory.newTransformer(xslt);
			if(parameters != null)
				for(Map.Entry<String, String> parameter : parameters.entrySet())
					transformer.setParameter(parameter.getKey(), parameter.getValue());
			
		} catch (final Throwable e) {
			log.error("Problems with transformer!\n" + xslt + "--" + name, e);
			log.error(xsltDump(xslt));
			throw new IllegalStateException(e);
		}
	}
	
	/**
	 * This class contains the login enabling imports of peer external resources for the current xslt (<xsl:import>)
	 * The method resolve() first looks for the resource within the file system; if this fails, it looks in the classpath.
	 * @author Andrea Mannocci
	 *
	 */
	private class ExternalResourceURIResolver implements URIResolver {
		private final String xsltBasePath;
		
		public ExternalResourceURIResolver(String xsltBasePath) {
			this.xsltBasePath = xsltBasePath;
		}
		@Override
		public Source resolve(String href, String base) throws TransformerException {
			String externalResource = this.xsltBasePath + href;
			try{
				log.debug("trying to load external resource from file system: " + externalResource);
				return new StreamSource(new File(externalResource));
			} catch (final Throwable e) {
				log.debug("trying to load external resource from file system: " + externalResource);
				return new StreamSource(getInputStream(new ClassPathResource(externalResource)));
			}
		}
	}
	
	private String xsltDump(Source xslt) {
		Transformer transformer;
		try {
			transformer = TransformerFactory.newInstance().newTransformer();
			transformer.setOutputProperty(OutputKeys.INDENT, "yes");
			StringWriter outWriter = new StringWriter();
			Result result = new StreamResult(outWriter);
			transformer.transform(xslt, result);
			StringBuffer sb = outWriter.getBuffer(); 
			return sb.toString();
		} catch (TransformerConfigurationException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (TransformerException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return "error dumping the xslt";
	}

	@Override
	public String evaluate(final K input) {
		try {
			final StringWriter output = new StringWriter();
			transformer.transform(toStream(input), new StreamResult(output));
			return output.toString();
		} catch (final TransformerException e) {
			log.error("cannot transform record", e);
			log.error(input.toString());
			return "";
		}
	}

	public abstract Source toStream(K input);
	
	public abstract String toString(K input);

	/**
	 * Used only to convert checked to unchecked exception.
	 * 
	 * @param xslt
	 * @return
	 */
	private static InputStream getInputStream(final Resource xslt) {
		try {
			return xslt.getInputStream();
		} catch (final IOException e) {
			throw new IllegalArgumentException(e);
		}
	}
	
	private static String getFileName(ClassPathResource xslt) {
		return xslt.getPath();
	}

	public Transformer getTransformer() {
		return transformer;
	}

	public void setTransformer(final Transformer transformer) {
		this.transformer = transformer;
	}

	public String getXsltName() {
		return xsltName;
	}

	public void setXsltName(String xsltName) {
		this.xsltName = xsltName;
	}

}
