// Copyright (C) 2010-2013 pdfcrowd.com
// 
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.

package pdfcrowd;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.AbstractCollection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Vector;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;

//
// Pdfcrowd API client.
// 
public class Client {

	private HashMap<String,String> fields;
	private boolean m_useSSL = false;

	//
	// Client constructor.
	// 
	// username - your username at Pdfcrowd
	// apikey   - your API key
	// 
	public Client(String username, String apikey) {
		init(username, apikey, API_HOSTNAME);
	}

	//
	// Converts a web page.
	//
	// uri       - a web page URL
	// outStream - an output stream (java.io.OutputStream)
	// 
	public void convertURI(String uri, OutputStream outStream) {
		byte[] data = encodePostData(uri);
		callAPI(data, "pdf/convert/uri/", outStream, URLENCODED);
	}

	//
	// Converts an in-memory html document.
	//
	// html      - a string containing a html document
	// outStream - an output stream (java.io.OutputStream)
	// 
	public void convertHtml(String html, OutputStream outStream) {
		byte[] data = encodePostData(html);
		callAPI(data, "pdf/convert/html/", outStream, URLENCODED);
	}

	//
	// Converts an html file.
	//
	// path      - a path to an html file
	// outStream - an output stream (java.io.OutputStream)
	// 
	public void convertFile(String path, OutputStream outStream) {
		ByteArrayOutputStream data = encodeMultipartPostData(path);
		callAPI(data, "pdf/convert/html/", outStream, MULTIPART);
	}

	//
	// Returns the number of available conversion tokens.
	// 
	public int numTokens() {
		byte[] data = encodePostData(null);
		ByteArrayOutputStream tokens = new ByteArrayOutputStream();
		callAPI(data, "user/" + fields.get("username") + "/tokens/", tokens, URLENCODED);
		return Integer.parseInt(tokens.toString());
	}

	public void useSSL(boolean val) {
		m_useSSL = val;
	}

	public void setUsername(String username) {
		fields.put("username", username);
	}

	public void setApiKey(String key) {
		fields.put("key", key);
	}

	public void setPageWidth(double value) {
		fields.put("width", doubleToString(value));
	}

	public void setPageWidth(String value) {
		fields.put("width", value);
	}


	public void setPageHeight(double value) {
		fields.put("height", doubleToString(value));
	}

	public void setPageHeight(String value) {
		fields.put("height", value);
	}

	public void setHorizontalMargin(double value) {
		fields.put("margin_right", doubleToString(value));
		fields.put("margin_left", doubleToString(value));
	}

	public void setHorizontalMargin(String value) {
		fields.put("margin_right", value);
		fields.put("margin_left", value);
	}

	public void setVerticalMargin(double value) {
		fields.put("margin_top", doubleToString(value));
		fields.put("margin_bottom", doubleToString(value));
	}

	public void setVerticalMargin(String value) {
		fields.put("margin_top", value);
		fields.put("margin_bottom", value);
	}

	public void setPageMargins(String top, String right, String bottom, String left) {
		fields.put("margin_top", top);
		fields.put("margin_right", right);
		fields.put("margin_bottom", bottom);
		fields.put("margin_left", left);
	}


	public void setEncrypted() {
		setEncrypted(true);
	}

	public void setEncrypted(boolean value) {
		fields.put("encrypted", value ? "true" : null);
	}

	public void setUserPassword(String pwd) {
		fields.put("user_pwd", pwd);
	}

	public void setOwnerPassword(String pwd) {
		fields.put("owner_pwd", pwd);
	}

	public void setNoPrint() {
		setNoPrint(true);
	}

	public void setNoPrint(boolean val) {
		fields.put("no_print", val ? "true" : null);
	}

	public void setNoModify() {
		setNoModify(true);
	}

	public void setNoModify(boolean val) {
		fields.put("no_modify", val ? "true" : null);
	}

	public void setNoCopy() {
		setNoCopy(true);
	}

	public void setNoCopy(boolean val) {
		fields.put("no_copy", val ? "true" : null);
	}

	// constants for setPageLayout()
	public static int SINGLE_PAGE = 1;
	public static int CONTINUOUS = 2;
	public static int CONTINUOUS_FACING = 3;

	public void setPageLayout(int value) {
		assert value > 0 && value <= 3;
		fields.put("page_layout", Integer.toString(value));
	}

	// constants for setPageMode()
	public static int NONE_VISIBLE = 1;
	public static int THUMBNAILS_VISIBLE = 2;
	public static int FULLSCREEN = 3;

	public void setPageMode(int value) {
		assert value > 0 && value <= 3;
		fields.put("page_mode", Integer.toString(value));
	}

	public void setFooterText(String value) {
		fields.put("footer_text", value);
	}

	public void enableImages() {
		enableImages(true);
	}

	public void enableImages(boolean value) {
		fields.put("no_images", value ? null : "true");
	}

	public void enableBackgrounds() {
		enableBackgrounds(true);
	}

	public void enableBackgrounds(boolean value) {
		fields.put("no_backgrounds", value ? null : "true");
	}

	public void setHtmlZoom(double value) {
		fields.put("html_zoom", Double.toString(value));
	}

	public void enableJavaScript() {
		enableJavaScript(true);
	}

	public void enableJavaScript(boolean value) {
		fields.put("no_javascript", value ? null : "true");
	}

	public void enableHyperlinks() {
		enableHyperlinks(true);
	}

	public void enableHyperlinks(boolean value) {
		fields.put("no_hyperlinks", value ? null : "true");
	}

	public void setDefaultTextEncoding(String value) {
		fields.put("text_encoding", value);
	}

	public void usePrintMedia() {
		usePrintMedia(true);
	}

	public void usePrintMedia(boolean value) {
		fields.put("use_print_media", value ? "true" : null);
	}

	public void setMaxPages(int value) {
		fields.put("max_pages", Integer.toString(value));
	}

	public void enablePdfcrowdLogo() {
		enablePdfcrowdLogo(true);
	}

	public void enablePdfcrowdLogo(boolean value) {
		fields.put("pdfcrowd_logo", value ? "true" : null);
	}

	// constants for setInitialPdfZoomType()
	public static int FIT_WIDTH = 1;
	public static int FIT_HEIGHT = 2;
	public static int FIT_PAGE = 3;

	public void setInitialPdfZoomType(int value) {
		assert value>0 && value<=3;
		fields.put("initial_pdf_zoom_type", Integer.toString(value));
	}

	public void setInitialPdfExactZoom(double value) {
		fields.put("initial_pdf_zoom_type", "4");
		fields.put("initial_pdf_zoom", Double.toString(value));
	}

	public Client(String username, String apikey, String api_hostname) {
		init(username, apikey, api_hostname);
	}

	public void setAuthor(String value) {
		fields.put("author", value);
	}

	public void setFailOnNon200(boolean value) {
		fields.put("fail_on_non200", value ? "true" : null);
	}

	public void setPdfScalingFactor(double value) {
		fields.put("pdf_scaling_factor", doubleToString(value));
	}

	public void setFooterHtml(String value) {
		fields.put("footer_html", value);
	}

	public void setFooterUrl(String value) {
		fields.put("footer_url", value);
	}

	public void setHeaderHtml(String value) {
		fields.put("header_html", value);
	}

	public void setHeaderUrl(String value) {
		fields.put("header_url", value);
	}

	public void setPageBackgroundColor(String value) {
		fields.put("page_background_color", value);
	}

	public void setTransparentBackground() {
		setTransparentBackground(true);
	}

	public void setTransparentBackground(boolean val) {
		fields.put("transparent_background", val ? "true" : null);
	}

	public void setPageNumberingOffset(int value) {
		fields.put("page_numbering_offset", Integer.toString(value));
	}

	public void setHeaderFooterPageExcludeList(String value) {
		fields.put("header_footer_page_exclude_list", value);
	}

	public void setWatermark(String url, double offset_x, double offset_y) {
		fields.put("watermark_url", url);
		fields.put("watermark_offset_x", Double.toString(offset_x));
		fields.put("watermark_offset_y", Double.toString(offset_y));
	}

	public void setWatermark(String url, String offset_x, String offset_y) {
		fields.put("watermark_url", url);
		fields.put("watermark_offset_x", offset_x);
		fields.put("watermark_offset_y", offset_y);
	}

	public void setWatermarkRotation(double angle) {
		fields.put("watermark_rotation", Double.toString(angle));
	}

	public void setWatermarkInBackground() {
		setWatermarkInBackground(true);
	}

	public void setWatermarkInBackground(boolean val) {
		fields.put("watermark_in_background", val ? "true" : null);
	}




	// ----------------------------------------------------------------------
	//                     Private stuff
	//                     

	private static String URLENCODED = new String("application/x-www-form-urlencoded");
	private static String MULTIPART_BOUNDARY = new String("----------ThIs_Is_tHe_bOUnDary_$");
	private static String MULTIPART = new String("multipart/form-data; boundary=" + MULTIPART_BOUNDARY);

	public static String API_HOSTNAME = new String("pdfcrowd.com");
	public static int API_HTTP_PORT = 80;
	public static int API_HTTPS_PORT = 443;
	private String api_hostname;


	private final static HostnameVerifier HOSTNAME_VERIFIER = new HostnameVerifier() {
		@Override
		public boolean verify(String hostname, SSLSession session) {
			return true;
			//return "pdfcrowd.com".equals(hostname);
		}
	};

	private void init(String username, String apikey, String apihost) {
		fields = new HashMap<String,String>();
		fields.put("username", username);
		fields.put("key", apikey);
		fields.put("pdf_scaling_factor", "1.0");
		fields.put("html_zoom", "200");
		api_hostname = apihost;
	}


	private String doubleToString(double val) {
		return String.format(Locale.US, "%.5f", val);
	}

	private static void copyStream(InputStream in, OutputStream out) throws IOException {
		byte[] buffer = new byte[8192];
		while (true) {
			int bytesRead = in.read(buffer, 0, 8192);
			if (bytesRead == -1) break;
			out.write(buffer, 0, bytesRead);
		}
	}

	private static String join(AbstractCollection<String> col, String delimiter) {
		if (col.isEmpty()) return "";
		Iterator<String> iter = col.iterator();
		StringBuffer buffer = new StringBuffer(iter.next());
		while(iter.hasNext()) buffer.append(delimiter).append(iter.next());
		return buffer.toString();
	}

	private String getBaseUri() {
		if (m_useSSL)
		{
			return String.format("https://%s:%d/api/", api_hostname, API_HTTPS_PORT);
		}
		else
		{
			return String.format("http://%s:%d/api/", api_hostname, API_HTTP_PORT);
		}
	}

	private HttpURLConnection getConnection(String uriSuffix, String contentType) throws IOException {
		try
		{
			URL url;
			HttpURLConnection conn = (HttpURLConnection)new URL(
					getBaseUri() + uriSuffix).openConnection();

			if (m_useSSL)
			{
				// BUG: sun-java6-bin: missing cacerts the trustAnchors parameter must be non-empty
				// http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=564903
				HttpsURLConnection ssl_conn = (HttpsURLConnection)conn;
				ssl_conn.setHostnameVerifier(HOSTNAME_VERIFIER);
			}
			conn.setRequestMethod("POST");
			conn.setDoOutput(true);
			conn.setRequestProperty("Content-Type", contentType);
			conn.setConnectTimeout(120000);
			return conn;
		}
		catch(MalformedURLException e)
		{
			throw new PdfcrowdError(e);
		}
	}

	private void callAPI(Object data, String uriSuffix,
			OutputStream outStream, String contentType) {
		try
		{
			HttpURLConnection conn = getConnection(uriSuffix, contentType);
			OutputStream wr = conn.getOutputStream();
			if (data instanceof byte[]) {
				//System.out.println(new String((byte[])data));
				wr.write((byte[])data);
			}
			else {
				//System.out.println(((ByteArrayOutputStream)data).toString());
				((ByteArrayOutputStream)data).writeTo(wr);
			}
			wr.flush();

			if (conn.getResponseCode() == 200) {
				InputStream rd = conn.getInputStream();
				copyStream(rd, outStream);
				rd.close();             
			}
			else {
				String errMsg;
				if (conn.getErrorStream() != null) {
					ByteArrayOutputStream errOut = new ByteArrayOutputStream();
					copyStream(conn.getErrorStream(), errOut);
					errMsg = errOut.toString();
				}
				else {
					errMsg = conn.getResponseMessage();
				}
				throw new PdfcrowdError(errMsg, conn.getResponseCode());
			}
			wr.close();
		}
		catch(IOException e)
		{
			throw new PdfcrowdError(e);
		}
	}

	private ByteArrayOutputStream encodeMultipartPostData(String filename) {
		try
		{
			Vector<String> body = new Vector<String>();
			Iterator<String> it = fields.keySet().iterator();
			ByteArrayOutputStream retval = new ByteArrayOutputStream();
			while(it.hasNext()) {
				String key = it.next();
				String val = fields.get(key);
				if (val != null)
				{
					body.add("--" + MULTIPART_BOUNDARY);
					body.add(String.format("Content-Disposition: form-data; name=\"%s\"", key));
					body.add("");
					body.add(val);
				}
			}
			// filename
			body.add("--" + MULTIPART_BOUNDARY);
			body.add(String.format("Content-Disposition: form-data; name=\"src\"; filename=\"%s\"", filename));
			body.add("Content-Type: application/octet-stream");
			body.add("");
			body.add("");
			retval.write(join(body, "\r\n").getBytes("UTF-8"));
			body.clear();

			// read file
			copyStream(new FileInputStream(filename), retval);

			body.add("");
			body.add("--" + MULTIPART_BOUNDARY);
			body.add("");
			retval.write(join(body, "\r\n").getBytes("UTF-8"));

			return retval;
		}
		catch(UnsupportedEncodingException e) {
			throw new PdfcrowdError(e);
		}
		catch(IOException e) {
			throw new PdfcrowdError(e);
		}
	}


	private byte[] encodePostData(String src) {

		Vector<String> body = new Vector<String>();
		Iterator it = fields.keySet().iterator();
		try
		{
			if (src != null)
				body.add(URLEncoder.encode("src", "UTF-8") + "=" +
						URLEncoder.encode(src, "UTF-8"));

			while(it.hasNext()) {
				Object key = it.next();
				Object val = fields.get(key);

				if (val != null)
				{
					body.add(URLEncoder.encode(key.toString(), "UTF-8") + "=" +
							URLEncoder.encode(val.toString(), "UTF-8"));
				}
			}
			return join(body, "&").getBytes("UTF-8");
		}
		catch(UnsupportedEncodingException e)
		{
			throw new PdfcrowdError(e);
		}
	}
}