package lu.tudor.santec.gecamed.core.gui.utils.xslfo;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import lu.tudor.santec.gecamed.core.gui.MainFrame;
import lu.tudor.santec.gecamed.core.gui.widgets.ErrorDialog;
import lu.tudor.santec.gecamed.formeditor.gui.FormEditorModule;
import lu.tudor.santec.i18n.Translatrix;

import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.apps.Fop;
import org.apache.fop.apps.FopFactory;
import org.apache.fop.apps.MimeConstants;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.Namespace;
import org.jdom.input.SAXBuilder;
import org.jdom.output.DOMOutputter;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ContentHandler;




public class TransFOmer
{
	/* ======================================== */
	//		 CONSTANTS
	/* ======================================== */
	
	public static final FileType[] SUPPORTED_OUTPUT_TYPES_FOR_TEXT = new FileType[] {
			new FileType("pdf", MimeConstants.MIME_PDF),
			new FileType("rtf", MimeConstants.MIME_RTF),
			new FileType("html", null)
//			new FileType("jpg", MimeConstants.MIME_JPEG),
//			new FileType("png", MimeConstants.MIME_PNG),
	};
//			MimeConstants.MIME_AFP, 
//			MimeConstants.MIME_AFP_ALT, 
//			MimeConstants.MIME_EPS, 
//			MimeConstants.MIME_FOP_AREA_TREE, 
//			MimeConstants.MIME_FOP_AWT_PREVIEW, 
//			MimeConstants.MIME_FOP_PRINT, 
//			MimeConstants.MIME_GIF, 
//			MimeConstants.MIME_JPEG, 
//			MimeConstants.MIME_MIF, 
//			MimeConstants.MIME_PCL, 
//			MimeConstants.MIME_PCL_ALT, 
//			MimeConstants.MIME_PDF, 
//			MimeConstants.MIME_PLAIN_TEXT, 
//			MimeConstants.MIME_PNG, 
//			MimeConstants.MIME_POSTSCRIPT, 
//			MimeConstants.MIME_RTF
//			MimeConstants.MIME_RTF_ALT1, 
//			MimeConstants.MIME_RTF_ALT2, 
//			MimeConstants.MIME_SVG, 
//			MimeConstants.MIME_TIFF, 
//			MimeConstants.MIME_XSL_FO 
	
	public static final String 		HTML_FORMAT 	= "html";
	
	public static final String 		UTF8			= "UTF-8";
	
	private static final String 	XSL_NS_PREFIX 	= "xsl";
	
	private static final String 	FO_NS_PREFIX 	= "fo";
	
	private static final Namespace 	XSL_NAMESPACE 	= Namespace.getNamespace(XSL_NS_PREFIX, "http://www.w3.org/1999/XSL/Transform");
	
	private static final Namespace 	FO_NAMESPACE 	= Namespace.getNamespace(FO_NS_PREFIX, 	"http://www.w3.org/1999/XSL/Format");
	
	private static final Pattern 	pattern 		= Pattern.compile("^\\s+(?=\\<)", Pattern.MULTILINE);
	
	/* ======================================== */
	//		 MEMBERS
	/* ======================================== */
	
	/** the logger Object for this class */
	private static Logger 		logger = Logger.getLogger(TransFOmer.class.getName());
	
	private static TransFOmer 	instance;
	
//	private static List<FileType> textOutputTypeList = Arrays.asList(SUPPORTED_OUTPUT_TYPES_FOR_TEXT);
	
	
	private FopFactory			fopFactory;
	
	private TransformerFactory	xsltFactory;
	
	private DocumentBuilder 	w3cBuilder;
	
	private DOMOutputter 		domOutputter;
	
	private SAXBuilder 			saxBuilder;
	
	private Source 				emptySource;
	
	
	
	/* ======================================== */
	//		 CONSTRUCTORS
	/* ======================================== */
	
	private TransFOmer ()
	{
		try
		{
			xsltFactory 	= TransformerFactory.newInstance();
			w3cBuilder 		= DocumentBuilderFactory.newInstance().newDocumentBuilder();
			domOutputter 	= new DOMOutputter();
			saxBuilder 		= new SAXBuilder();
			fopFactory 		= FopFactory.newInstance();
			fopFactory.setStrictValidation(false);
			emptySource 	= new DOMSource(w3cBuilder.newDocument());
	//		xmlOutputter 	= new XMLOutputter(Format.getCompactFormat());
		} 
		catch (ParserConfigurationException e)
		{
			logger.log(Level.ERROR, e.getMessage(), e);
		}
	}
	
	
	public static TransFOmer getInstance ()
	{
		if (instance == null)
			instance = new TransFOmer();
		
		return instance;
	}
	
	
	
	/* ======================================== */
	//		 CLASS BODY
	/* ======================================== */
	
	public byte[] transFO (Document xmlData, Document xslTemplate, String outputFormat)
	{
		byte[] 	result;
		Node 	templateDoc;
		Node 	dataDoc;
		
		try
		{
			templateDoc = domOutputter.output(xmlData);
			dataDoc 	= domOutputter.output(xslTemplate);
			
			result 		= transFO(dataDoc, templateDoc, outputFormat);
		} 
		catch (Exception e)
		{
			logger.log(Level.ERROR, e.getMessage(), e);
			result 		= null;
		}
		return result;
	}
	
	
	public byte[] transFO (String xmlData, String xslTemplate, String outputFormat)
	{
		byte[] 	result;
		Node 	templateDoc;
		Node 	dataDoc;
		
		try
		{
			templateDoc = loadXMLFrom(xslTemplate);
			dataDoc 	= loadXMLFrom(xmlData);
			
			result 		= transFO(dataDoc, templateDoc, outputFormat);
		} 
		catch (Exception e)
		{
			logger.log(Level.ERROR, e.getMessage(), e);
 			ErrorDialog.showErrorDialog(MainFrame.getInstance(), 
 					Translatrix.getTranslationString("Error while transforming XSL"), 
 					Translatrix.getTranslationString("An error occured while GECAMed tried to transform you XSL Document.")
 						+ "\n\n" + e.getMessage(), 
 					e);
			result = null;
		}
		return result;
	}
	
	
	public byte[] transFO (InputStream xmlData, InputStream xslTemplate, String outputFormat)
	{
		byte[] 	result;
		Node 	templateDoc;
		Node 	dataDoc;
		
		try
		{
			templateDoc = loadXMLFrom(xslTemplate);
			dataDoc 	= loadXMLFrom(xmlData);
			
			result 		= transFO(dataDoc, templateDoc, outputFormat);
		} 
		catch (Exception e)
		{
			logger.log(Level.ERROR, e.getMessage(), e);
			result = null;
		}
		finally 
		{
			try
			{
				xslTemplate.close();
				xmlData.close();
			} 
			catch (IOException e)
			{
				logger.log(Level.ERROR, e.getMessage(), e);
			}
		}
		return result;
	}
	
	public byte[] transFO (InputStream xmlData, String xslTemplate, String outputFormat)
	{
		byte[] 	result;
		Node 	templateDoc;
		Node 	dataDoc;
		
		try
		{
			templateDoc = loadXMLFrom(xslTemplate);
			dataDoc 	= loadXMLFrom(xmlData);
			
			result 		= transFO(dataDoc, templateDoc, outputFormat);
		} 
		catch (Exception e)
		{
			logger.log(Level.ERROR, e.getMessage(), e);
			result = null;
		}
		finally 
		{
			try
			{
				//xslTemplate.close();
				xmlData.close();
			} 
			catch (IOException e)
			{
				logger.log(Level.ERROR, e.getMessage(), e);
			}
		}
		return result;
	}
	
	public byte[] transFO (Node xmlData, Node xslTemplate, String fileExtension)
	{
		boolean 	useFop 		= fileExtension != null && nodeContainsNode(xslTemplate, "fo:root");
		FileType 	fileType 	= null;
		FileType 	supportedType;
		
		if (fileExtension == null)
			fileExtension = HTML_FORMAT;
		
		if (fileExtension != null && useFop)
		{
			for (int index = 0; index < SUPPORTED_OUTPUT_TYPES_FOR_TEXT.length && fileType == null; index++)
			{
				supportedType = SUPPORTED_OUTPUT_TYPES_FOR_TEXT[index];
				if (supportedType.fileExtension.toLowerCase().equals(fileExtension.toLowerCase()))
					fileType = new FileType(supportedType.getExtension(), supportedType.getMimeType());
			}
		}
		
		if (fileType == null)
			fileType = new FileType(fileExtension);
		
		/* In case the fo:root-element does NOT exist in the XML structure or the output 
		 * format is in the list of supported output formats, use this method
		 */
		if (!useFop || fileType.getMimeType() != null)
			return transFOmToText(xmlData, xslTemplate, fileType, useFop);
		
		/* If the fo:root-element exists in the XML structure and the output format is HTML 
		 * or not defined use this method 
		 */
		if (useFop && fileType.getExtension().toLowerCase().equals(HTML_FORMAT))
			return transFOmToHtml(xmlData, xslTemplate);
		
		throw new UnsupportedOperationException("The Format \""+fileType+"\" is not supported.");
	}
	
	
	private byte[] transFOmToHtml (Node xmlData, Node xslTemplate)
	{
		ByteArrayOutputStream 	outputStream 	= new ByteArrayOutputStream();
		InputStream 			fo2htmlStream 	= null;
		
		Source 		xmlDataSource;
		Source 		templateSource;
		DOMResult 	foDataResult;
		Node 		foDataDoc;
		Source 		foDataSource;
		Result 		outputResult;
		Source 		fo2HtmlSource;
		byte[] 		result;
//		String		result;
		Transformer xmlTransformer;
		
		try
		{
			// replace the XSL data in the template by the XML data
			xmlDataSource 	= new DOMSource(xmlData);
			templateSource 	= new DOMSource(xslTemplate);
			foDataDoc 		= w3cBuilder.newDocument();
			foDataResult 	= new DOMResult(foDataDoc);
			
			xmlTransformer 	= xsltFactory.newTransformer(templateSource);
			xmlTransformer.transform(xmlDataSource, foDataResult);
			foDataDoc 		= foDataResult.getNode();
			
			// transform the FO-XSL document to HTML using the "fo2html" XSL-file
			fo2htmlStream 	= FormEditorModule.class.getResourceAsStream("resources/fo2html.xsl");
			fo2HtmlSource 	= new StreamSource(fo2htmlStream);
			foDataSource 	= new DOMSource(foDataDoc);
			outputResult 	= new StreamResult(outputStream);
			
			xmlTransformer 	= xsltFactory.newTransformer(fo2HtmlSource);
			xmlTransformer.transform(foDataSource, outputResult);
			result 			= outputStream.toByteArray();
			result			= new String(result).replace("^\\s+\\<", "").getBytes();
			
			Matcher matcher = pattern.matcher(new String(result));
			result = matcher.replaceAll("").getBytes();
						
//			result			= outputStream.toString(UTF8);
		}
		catch (Exception e)
		{
			logger.log(Level.ERROR, e.getMessage(),e);
			result = null;
		}
		finally
		{
			// close the streams
			try
			{
				if (outputStream != null)
					outputStream.close();
				if (fo2htmlStream != null)
					fo2htmlStream.close();
			} 
			catch (Exception e)
			{
				logger.log(Level.WARN,"Problem while trying to close the streams.\n" +
						e.getMessage() + "\n" + 
						e.getLocalizedMessage());
			}
		}
		
		return result;
	}
	
	
	private byte[] transFOmToText (Node xmlData, Node xslTemplate, FileType outputFormat, boolean useFop)
	{
		ByteArrayOutputStream 	output = new ByteArrayOutputStream();
		InputStream 			iStream = null;
		OutputStream 			oStream = null;
		Fop 					fop;
		Result 					outputTarget;
		Source 					templateSource;
		Source 					xmlDataSource;
		ContentHandler 			renderHandler;
		Transformer 			foTransformer;
		Document 				document;
		Element 				root;
		byte[] 					bytes;
//		String					result	= null;
		
		try 
		{
			outputTarget 	= new StreamResult(output);
			templateSource 	= new DOMSource(xslTemplate);
			xmlDataSource 	= new DOMSource(xmlData);
			foTransformer  	= xsltFactory.newTransformer(templateSource);
			foTransformer.transform(xmlDataSource, outputTarget);
			
			bytes 			= output.toByteArray();
			
			if (useFop)
			{
				// create the source out of the result of the last operation
				iStream 		= new ByteArrayInputStream(bytes);
				document 		= saxBuilder.build(iStream);
				
				// add the XSL namespace to the root, as the old definition was deleted, if there was one
				root 			= document.getRootElement();
				if (root.getNamespace(XSL_NS_PREFIX) == null)
					root.addNamespaceDeclaration(XSL_NAMESPACE);
				if (root.getNamespace(FO_NS_PREFIX) == null)
					root.addNamespaceDeclaration(FO_NAMESPACE);
					
				templateSource 	= new DOMSource(domOutputter.output(document));
				
				// create the result by using apache FOP
				output.close();
				output 			= new ByteArrayOutputStream();
				fop 			= createFop("GECAMed Form", outputFormat.mimeType, output);
				renderHandler 	= fop.getDefaultHandler();
				outputTarget 	= new SAXResult(renderHandler);
				
				// perform the transformation
				foTransformer  	= xsltFactory.newTransformer(templateSource);
				foTransformer.transform(emptySource, outputTarget);
				
				bytes 			= output.toByteArray();
//				result			= output.toString(UTF8);
			}
		}
		catch (Exception e)
		{
 			logger.log(Level.ERROR, e.getMessage(), e);
 			ErrorDialog.showErrorDialog(MainFrame.getInstance(), 
 					Translatrix.getTranslationString("Error while transforming XSL"), 
 					Translatrix.getTranslationString("An error occured while GECAMed tried to transform you XSL Document.")
 						+ "\n\n" + e.getMessage(), 
 					e);
			bytes = null;
		}
		finally
		{
			// close the streams
			try
			{
				if (output != null)
					output.close();
				if (iStream != null)
					iStream.close();
				if (oStream != null)
					oStream.close();
			} 
			catch (Exception e)
			{
				logger.log(Level.WARN,"Problem while trying to close the streams.\n" +
						e.getMessage() + "\n" + 
						e.getLocalizedMessage());
			}
		}
		
		return bytes;
//		return result;
	}
	
	
//	protected byte[] transformWithXslt (Node xmlData, Node xslTemplate)
//	{
//		ByteArrayOutputStream output = new ByteArrayOutputStream();
//		
//		Result 			outputResult;
//		Transformer 	transformer;
//		Source 			templateSource;
//		Source 			xmlDataSource;
//		
//		try
//		{
//			templateSource 	= new DOMSource(xslTemplate);
//			xmlDataSource 	= new DOMSource(xmlData);
//			outputResult 	= new StreamResult(output);
//			
//			transformer 	= xsltFactory.newTransformer(templateSource);
//			transformer.transform(xmlDataSource, outputResult);
//			
//			return output.toByteArray();
//		} 
//		catch (Exception e)
//		{
//			logger.log(Level.ERROR, e.getMessage(), (Throwable)e);
//		}
//		return null;
//	}
	
	
	
	/* ======================================== */
	//		 HELP METHODS
	/* ======================================== */
	
//	private static XMLOutputter xmlFormatter = new XMLOutputter(Format.getPrettyFormat());
//	
//	public static void printDocument (Document document)
//	{
//		System.out.println("  ------------------------------------------------------  ");
//		System.out.println(xmlFormatter.outputString(document));
//		System.out.println("__________________________________________________________");
//	}
	
	
	private Fop createFop (String title, String outputFormat, OutputStream oStream)
	{
		try 
		{
			FOUserAgent userAgent;
			Fop 		fop;
			
//			if (!textOutputTypeList.contains(outputFormat))
//				throw new Exception ("Unsupported output format: " + outputFormat);
			
			userAgent = fopFactory.newFOUserAgent();
			userAgent.setTitle(title);
			userAgent.setProducer ("GECAMed");
			userAgent.setCreationDate (new Date());
			
			fop = fopFactory.newFop(outputFormat, userAgent, oStream);
			
			return fop;
		}
		catch (Exception e) 
		{
			logger.log(Level.ERROR, e.getMessage(), e);
			return null;
		}
	}
	
	
	private boolean nodeContainsNode (Node root, String searchName)
	{
		boolean 	found = false;
		NodeList 	children;
		
		if (root.getNodeName().equals(searchName))
			found = true;
		else
		{
			children = root.getChildNodes();
			for (int index = 0; index < children.getLength() && !found; index++)
			{
				found = nodeContainsNode(children.item(index), searchName);
			}
		}
		
		return found;
	}
	
	
	public Node loadXMLFrom (Document xml) throws Exception
	{
		return domOutputter.output(xml);
	}
	
	
	public Node loadXMLFrom (InputStream xml) throws Exception
	{
		Document doc = saxBuilder.build(xml);
		return domOutputter.output(doc);
		
		/* THIS DOESN'T WORK - God knows why!
		 * The document themes to be well formed, but while transforming
		 * there appears an error
		 */
//		return w3cBuilder.parse(xml);
	}
	
	
	public Node loadXMLFrom (String xml) throws Exception 
	{
		Reader 		reader = new StringReader(xml);
		Document 	doc = saxBuilder.build(reader);
		return domOutputter.output(doc);

		/* THIS DOESN'T WORK - God knows why!
		 * The document themes to be well formed, but while transforming
		 * there appears an error
		 */
//		StringReader 	reader 	= new StringReader(xml);
//		InputSource 	is 		= new InputSource(reader);
//		return w3cBuilder.parse(is);
	}
	
	
	
//	public static void main(String[] args) throws Exception
//	{
//		TransFOmer 		transFOmer = TransFOmer.getInstance();
//		File 			file;
//		byte[] 			fileData;
//		FileInputStream templateStream;
//		FileInputStream dataStream;
//		
//		file  			= new File("E:\\xsl-fo_template.xsl");
//		templateStream 	= new FileInputStream(file);
//		file  			= new File("E:\\xml_data.xml");
//		dataStream 		= new FileInputStream(file);
//		
//		
//		String fileExtension = HTML_FORMAT;
//		fileData 	= transFOmer.transFO(dataStream, templateStream, fileExtension);
//		
//		System.out.println(new String(fileData));
//		
//		file 		= File.createTempFile("test", "."+fileExtension);
//		file 		= FileUtils.createFile(fileData, file);
//		Desktop.getDesktop().open(file);
////		Desktop.getDesktop().print(file);
//	}
	
	
	
	/* ======================================== */
	// 		CLASS BODY
	/* ======================================== */
	
	public static class FileType 
	{
		/* ======================================== */
		// 		MEMBERS
		/* ======================================== */
		
		private String fileExtension;
		
		private String mimeType;
		
		
		
		/* ======================================== */
		// 		CONSTRUCTORS
		/* ======================================== */
		
		public FileType(String fileExtension, String mimeType)
		{
			super();
			this.fileExtension 	= fileExtension;
			this.mimeType 		= mimeType;
		}
		
		
		public FileType(String fileExtension)
		{
			this(fileExtension, null);
		}
		
		
		
		/* ======================================== */
		// 		GETTER & SETTER
		/* ======================================== */
		
		public String getExtension ()
		{
			return fileExtension;
		}
		
		
		public String getMimeType ()
		{
			return mimeType;
		}
		
		
		@Override
		public boolean equals(Object obj)
		{
			if (obj == null)
			{
				return false;
			}
			else if (obj instanceof FileType)
			{
				FileType type = (FileType) obj;
				return mimeType != null ? mimeType.equals(type.mimeType) : fileExtension.equals(type.fileExtension);
			} 
			else if (obj instanceof String)
			{
				return obj.equals(fileExtension);
			}
			else return super.equals(obj);
		}
		
		
		@Override
		public String toString()
		{
			return fileExtension;
		}
	}
}

