/*
 * This file is part of Nuts Framework.
 * Copyright (C) 2009 Nuts Develop Team.
 *
 * Nuts Framework is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License any later version.
 * 
 * Nuts Framework is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Nuts Framework. If not, see <http://www.gnu.org/licenses/>.
 */
package nuts.exts.struts2.actions;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import nuts.core.collections.ExpireMap;
import nuts.core.io.IOUtils;
import nuts.core.lang.ExceptionUtils;
import nuts.core.lang.StringUtils;
import nuts.core.net.HttpClientAgent;
import nuts.core.servlet.HttpServletSupport;
import nuts.core.servlet.HttpServletUtils;
import nuts.core.servlet.URLHelper;
import nuts.exts.struts2.NutsStrutsConstants;
import nuts.exts.struts2.util.StrutsContextUtils;

import org.apache.commons.lang.ArrayUtils;
import org.apache.http.impl.cookie.BasicClientCookie;
import org.w3c.dom.Document;
import org.w3c.tidy.Tidy;
import org.xhtmlrenderer.extend.UserAgentCallback;
import org.xhtmlrenderer.layout.SharedContext;
import org.xhtmlrenderer.pdf.ITextFSImage;
import org.xhtmlrenderer.pdf.ITextOutputDevice;
import org.xhtmlrenderer.pdf.ITextRenderer;
import org.xhtmlrenderer.pdf.PDFAsImage;
import org.xhtmlrenderer.resource.CSSResource;
import org.xhtmlrenderer.resource.ImageResource;
import org.xhtmlrenderer.resource.XMLResource;
import org.xhtmlrenderer.util.XRLog;

import com.lowagie.text.Image;
import com.lowagie.text.Rectangle;
import com.lowagie.text.pdf.BaseFont;
import com.lowagie.text.pdf.PdfReader;
import com.opensymphony.xwork2.inject.Inject;


/**
 */
@SuppressWarnings("serial")
public class Html2PdfAction extends CommonAction {

	protected static ExpireMap<String, Object> expire = new ExpireMap<String, Object>(
		new WeakHashMap<String, Object>(), 5 * 60 * 1000);

	protected static Map<String, Object> cache = Collections.synchronizedMap(expire);

	protected HttpClientAgent agent;
	protected ITextRenderer renderer;
	
	protected String fonts;
	protected String url;
	protected String charset;
	
	/**
	 * @return the fonts
	 */
	public String getFonts() {
		return fonts;
	}

	/**
	 * @param fonts the fonts to set
	 */
	@Inject(value=NutsStrutsConstants.NUTS_FONTS_PATH, required=false)
	public void setFonts(String fonts) {
		this.fonts = fonts;
	}

	/**
	 * @return the url
	 */
	public String getUrl() {
		return url;
	}

	/**
	 * @param url the url to set
	 */
	public void setUrl(String url) {
		this.url = url;
	}

	/**
	 * @return the charset
	 */
	public String getCharset() {
		return charset;
	}

	/**
	 * @param charset the charset to set
	 */
	public void setCharset(String charset) {
		this.charset = StringUtils.stripToNull(charset);
	}

	/**
	 * @param ms cache expire milliseconds to set
	 */
	public static void setCacheExpire(int ms) {
		expire.setExpire(ms);
	}

	private class ResourceLoaderUserAgent implements UserAgentCallback {
		private SharedContext _sharedContext;
		private ITextOutputDevice _outputDevice;
		private String _baseURL;

		public ResourceLoaderUserAgent(ITextOutputDevice outputDevice,
				SharedContext sharedContext) {
			super();
			_outputDevice = outputDevice;
			_sharedContext = sharedContext;
		}

		protected InputStream resolveStream(String uri) {
			return new ByteArrayInputStream(resolveData(uri));
		}

		protected byte[] resolveData(String uri) {
			try {
				agent.doGet(uri);
				return agent.getResponseContent();
			}
			catch (IOException e) {
				log.warn("GET " + uri, e);
				return new byte[0];
			}
		}

		/**
		 * Retrieves the CSS at the given URI. This is a synchronous call.
		 * 
		 * @param uri Location of the CSS
		 * @return A CSSResource for the content at the URI.
		 */
		public CSSResource getCSSResource(String uri) {
			Object v = cache.get(uri);
			if (v instanceof byte[]) {
				return new CSSResource(new ByteArrayInputStream((byte[])v));
			}

			v = resolveData(uri);
			cache.put(uri, v);

			return new CSSResource(new ByteArrayInputStream((byte[])v));
		}

		/**
		 * Retrieves the Image at the given URI. This is a synchronous call.
		 * 
		 * @param uri Location of the image
		 * @return An ImageResource for the content at the URI.
		 */
		public ImageResource getImageResource(String uri) {
			uri = resolveURI(uri);
			
			Object v = cache.get(uri);
			if (v instanceof ImageResource) {
				return (ImageResource)v;
			}

			ImageResource resource = null;
			InputStream is = resolveStream(uri);
			if (is != null) {
				try {
					URL url = new URL(uri);
					if (url.getPath() != null
							&& url.getPath().toLowerCase().endsWith(".pdf")) {
						PdfReader reader = _outputDevice.getReader(url);
						PDFAsImage image = new PDFAsImage(url);
						Rectangle rect = reader.getPageSizeWithRotation(1);
						image.setInitialWidth(rect.getWidth()
								* _outputDevice.getDotsPerPoint());
						image.setInitialHeight(rect.getHeight()
								* _outputDevice.getDotsPerPoint());
						resource = new ImageResource(uri, image);
					}
					else {
						Image image = Image.getInstance(agent.getResponseContent());
						float factor = _sharedContext.getDotsPerPixel();
						image.scaleAbsolute(image.getPlainWidth() * factor,
								image.getPlainHeight() * factor);
						resource = new ImageResource(uri, new ITextFSImage(
								image));
					}
				}
				catch (Exception e) {
					log.error(
							"Can't read image file; unexpected problem for URI '"
									+ uri + "'", e);
				}
				finally {
					IOUtils.closeQuietly(is);
				}
			}

			if (resource == null) {
				resource = new ImageResource(uri, null);
			}

			cache.put(uri, resource);
			return resource;
		}

		/**
		 * Retrieves the XML at the given URI. This is a synchronous call.
		 * 
		 * @param uri Location of the XML
		 * @return A XMLResource for the content at the URI.
		 */
		public XMLResource getXMLResource(String uri) {
			Object v = cache.get(uri);
			if (v instanceof XMLResource) {
				return (XMLResource)v;
			}

			XMLResource x = XMLResource.load(resolveStream(uri));
			cache.put(uri, x);
			return x;
		}

		/**
		 * Retrieves a binary resource located at a given URI and returns its
		 * contents as a byte array or <code>null</code> if the resource could
		 * not be loaded.
		 * 
		 * @param uri uri 
		 * @return binary resource
		 */
		public byte[] getBinaryResource(String uri) {
			Object v = cache.get(uri);
			if (v instanceof byte[]) {
				return (byte[])v;
			}

			v = resolveData(uri);
			cache.put(uri, v);

			return (byte[])v;
		}

		/**
		 * Normally, returns true if the user agent has visited this URI.
		 * UserAgent should consider if it should answer truthfully or not for
		 * privacy reasons.
		 * 
		 * @param uri A URI which may have been visited by this user agent.
		 * @return The visited value
		 */
		public boolean isVisited(String uri) {
			return false;
		}

		/**
		 * Does not need to be a correct URL, only an identifier that the
		 * implementation can resolve.
		 * 
		 * @param url A URL against which relative references can be resolved.
		 */
		public void setBaseURL(String url) {
			_baseURL = url;
		}

		/**
		 * @return the base uri, possibly in the implementations private
		 *         uri-space
		 */
		public String getBaseURL() {
			return _baseURL;
		}

		/**
		 * Used to find a uri that may be relative to the BaseURL. The returned
		 * value will always only be used via methods in the same implementation
		 * of this interface, therefore may be a private uri-space.
		 * 
		 * @param uri an absolute or relative (to baseURL) uri to be resolved.
		 * @return the full uri in uri-spaces known to the current
		 *         implementation.
		 */
		public String resolveURI(String uri) {
			if (uri == null)
				return null;
			String ret = null;
			if (_baseURL == null) {// first try to set a base URL
				try {
					URL result = new URL(uri);
					setBaseURL(result.toExternalForm());
				}
				catch (MalformedURLException e) {
					try {
						setBaseURL(new File(".").toURI().toURL().toExternalForm());
					}
					catch (Exception e1) {
						XRLog.exception("The default NaiveUserAgent doesn't know how to resolve the base URL for "
								+ uri);
						return null;
					}
				}
			}
			// test if the URI is valid; if not, try to assign the base url as
			// its parent
			try {
				return new URL(uri).toString();
			}
			catch (MalformedURLException e) {
				XRLog.load("Could not read "
						+ uri
						+ " as a URL; may be relative. Testing using parent URL "
						+ _baseURL);
				try {
					URL result = new URL(new URL(_baseURL), uri);
					ret = result.toString();
				}
				catch (MalformedURLException e1) {
					XRLog.exception("The default NaiveUserAgent cannot resolve the URL "
							+ uri + " with base URL " + _baseURL);
				}
			}
			return ret;
		}
	}
	
	protected void createHttpClientAgent() {
		agent = new HttpClientAgent();
		agent.getRequestHeader().put("flying-saucer", "__pdf");

		HttpServletRequest request = StrutsContextUtils.getServletRequest();

		String domain = URLHelper.getUrlDomain(url);
		String server = request.getServerName();

		if (domain.equalsIgnoreCase(server)) {
			String jsid = HttpServletUtils.getCookieValue(request, "JSESSIONID");
			if (StringUtils.isNotEmpty(jsid)) {
				BasicClientCookie cookie = new BasicClientCookie("JSESSIONID", jsid);
				cookie.setVersion(1);
				cookie.setDomain(domain);
				cookie.setPath(request.getContextPath());
				agent.getCookieStore().addCookie(cookie);
			}
		}
	}
	
	protected void createITextRenderer(HttpClientAgent agent) throws Exception {
		renderer = new ITextRenderer();

		if (StringUtils.isNotEmpty(fonts)) {
			File fontdir;
			if (fonts.startsWith("web://")) {
				fontdir = new File(StrutsContextUtils.getServletContext().getRealPath(fonts.substring(5)));
			}
			else {
				fontdir = new File(fonts);
			}

			if (fontdir.isDirectory()) {
				File[] files = fontdir.listFiles(new FilenameFilter() {
					public boolean accept(File dir, String name) {
						String lower = name.toLowerCase();
						return lower.endsWith(".otf") || lower.endsWith(".ttf") || lower.endsWith(".ttc");
					}
				});

				if (ArrayUtils.isNotEmpty(files)) {
					for (File file : files) {
						renderer.getFontResolver().addFont(
								file.getAbsolutePath(), BaseFont.IDENTITY_H,
								BaseFont.EMBEDDED);
					}
				}
			}
		}


		ResourceLoaderUserAgent callback = new ResourceLoaderUserAgent(
			renderer.getOutputDevice(), renderer.getSharedContext());
		renderer.getSharedContext().setUserAgentCallback(callback);
	}
	
	protected byte[] html2pdf() throws Exception {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();

		createHttpClientAgent();
		createITextRenderer(agent);
		
		agent.doGet(url);

		Tidy tidy = new Tidy();
		tidy.setXHTML(true);
		
		Document doc = null;
		String charset = null;
		if (StringUtils.isEmpty(this.charset)) {
			charset = agent.getResponseCharset();
		}
		else {
			charset = this.charset;
		}

		if (StringUtils.isEmpty(charset)) {
			doc = tidy.parseDOM(agent.getResponseStream(), null);
		}
		else {
			String responseText = new String(agent.getResponseContent(), charset);
			doc = tidy.parseDOM(new StringReader(responseText), null);
		}

		renderer.setDocument(doc, url);
		renderer.layout();
		renderer.createPDF(baos);
		renderer.finishPDF();

		return baos.toByteArray();
	}

	protected boolean normalizeUrl() {
		if (url == null) {
			url = getText("url-default");
			return false;
		}
		
		url = StringUtils.stripToNull(url);
		if (url == null) {
			return false;
		}

		int i = url.indexOf("://");
		if (i < 0) {
			url = "http://" + url;
		}
		return true;
	}

	private String getFileNameFromUrl(String url) {
		String fn = StringUtils.stripEnd(url, "/");
		int i = fn.lastIndexOf('/');
		if (i >= 0) {
			fn = fn.substring(i + 1);
		}
		i = fn.lastIndexOf('?');
		if (i >= 0) {
			fn = fn.substring(0, i);
		}
		i = fn.lastIndexOf(';');
		if (i >= 0) {
			fn = fn.substring(0, i);
		}
		return fn;
	}
	
	/**
	 * execute
	 * 
	 * @return INPUT
	 * @throws Exception if an error occurs
	 */
	public String execute() throws Exception {
		if (normalizeUrl()) {
			try {
				byte[] pdf = html2pdf();
				
				HttpServletRequest request = StrutsContextUtils.getServletRequest();
				HttpServletResponse response = StrutsContextUtils.getServletResponse();

				HttpServletSupport hsrs = new HttpServletSupport(request, response);
				
				hsrs.setContentLength(Integer.valueOf(pdf.length));
				hsrs.setContentType("application/pdf");
				hsrs.setFileName(getFileNameFromUrl(url) + ".pdf");
				hsrs.setNoCache(true);

				hsrs.writeResponseHeader();
				hsrs.writeResponseData(pdf);

				return NONE;
			}
			catch (Throwable e) {
				log.error("html2pdf", e);
				addActionError(ExceptionUtils.getStackTrace(e));
			}
		}

		return INPUT;
	}

}
