// Copyright (C) 2016 RunTex LLC
// 
// 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 com.runpdf;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;

import com.runpdf.libs.apache.StringEscapeUtils;

/**
 * This class contains functionality of the RunPDF export web service client.
 * 
 */

public class ExportClient
{
    private HashMap<String, String> parameterMap = new HashMap<String, String>();
    private int                     statusCode   = 200;
    private String                  serviceURL   = Constants.SERVICE_URL;

    /**
     * Client constructor.
     * 
     * @param exportKey
     *            A unique key assigned to all registered accounts for request
     *            authentication.
     */

    public ExportClient(String exportKey)
    {
        setDeliveryStream();
        setExportKey(exportKey);
    }

    /**
     * Client constructor.
     * 
     * @param exportKey
     *            A unique key assigned to all registered accounts for request
     *            authentication.
     * 
     * @param serviceURL
     *            The URL to RunPDF export web service.
     */

    public ExportClient(String exportKey, String serviceURL)
    {
        this.serviceURL = serviceURL;
        setDeliveryStream();
        setExportKey(exportKey);
    }

    /**
     * Exports HTML content to PDF, copies the stream of the resultant document
     * to the target OutputStream and returns the size of the stream.
     * 
     * @param outputStream
     *            The target OutputStream.
     * 
     * @return Size of the resultant document stream.
     * 
     * @throws Exception
     *             The exception is thrown in case of failure to execute. The
     *             exception message is set to a specific error code (see
     *             com.runpdf.Constants.ERROR_CODE_MESSAGE_MAP).
     */

    public int export(OutputStream outputStream) throws Exception
    {
        String parameters = getParameters();
        if (parameters == null)
        {
            throw new Exception(Constants.ERROR_INPUT);
        }

        byte[] parametersBytes = parameters.toString().getBytes("UTF-8");

        URL url = new URL(serviceURL);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("POST");
        conn.setRequestProperty("Content-Type""application/x-www-form-urlencoded");
        conn.setRequestProperty("Content-Length", String.valueOf(parametersBytes.length));
        conn.setDoOutput(true);
        conn.getOutputStream().write(parametersBytes);

        statusCode = conn.getResponseCode();

        if (statusCode != 200)
        {
            throw new Exception("" + statusCode);
        }

        InputStream inputStream = conn.getInputStream();
        return copyStream(inputStream, outputStream);
    }

    /**
     * Returns an error message based on the specified error code.
     * 
     * @param errorCode
     *            The error code.
     * 
     * @return An error message.
     */

    public String getErrorMessage(String errorCode)
    {
        for (String[] errorCodeMessage : Constants.ERROR_CODE_MESSAGE_MAP)
        {
            if (errorCodeMessage[0].equals(errorCode))
            {
                return errorCodeMessage[1];
            }
        }

        return "Unexpected error [" + errorCode + "]. Please, verify your input and try again later.";
    }

    /**
     * Sets source web page URL to convert.
     * 
     * @param sourceUrl
     *            URL of the web page to convert.
     */

    public void setSourceUrl(String sourceUrl)
    {
        updateParameterMap(Constants.SOURCE_URL, sourceUrl);
    }

    /**
     * Sets URLs of the primary and secondary web pages to convert.
     * 
     * @param sourceUrl
     *            URL of the primary web page to convert.
     * @param sourceUrl2
     *            URL of the secondary web page to convert.
     */

    public void setSourceUrl(String sourceUrl, String sourceUrl2)
    {
        setSourceUrl(sourceUrl);
        setSourceUrl2(sourceUrl2);
    }

    /**
     * Sets HTML source string.
     * 
     * @param sourceHtml
     *            HTML string to convert.
     */

    public void setSourceHtml(String sourceHtml)
    {
        updateParameterMap(Constants.SOURCE_HTML, sourceHtml);
    }

    /**
     * Sets HTML source string and the base URL.
     * 
     * @param sourceHtml
     *            HTML string to convert.
     * @param baseUrl
     *            URL of the web page to process CSS when converting HTML
     *            source.
     */

    public void setSourceHtml(String sourceHtml, String baseUrl)
    {
        setSourceHtml(sourceHtml);
        setBaseUrl(baseUrl);
    }

    /**
     * Sets username and password credentials for NTLM authentication. Note:
     * only NTLM authentication is supported.
     * 
     * @param username
     *            The username credential.
     * @param password
     *            The password credential.
     */

    public void setAuthentication(String username, String password)
    {
        setAuthenticationUsername(username);
        setAuthenticationPassword(password);
    }

    /**
     * Sets if to present the document on print media style.
     * 
     * @param value
     *            Boolean value. Default is false.
     */

    public void setMediaTypePrint(boolean value)
    {
        updateParameterMap(Constants.MEDIA_TYPE_PRINT, value);
    }

    /**
     * Sets the standard of the resultant PDF document.
     * 
     * @param standard
     *            The standard of the resultant PDF document. The values are
     *            available in com.runpdf.Constants (Example:
     *            Constants.STANDARD_PDF_A;). Default format is STANDARD_PDF.
     */

    public void setStandard(String standard)
    {
        updateParameterMap(Constants.STANDARD, standard);
    }

    /**
     * Sets if to avoid image break between pages.
     * 
     * @param value
     *            Boolean value. Default is false.
     */

    public void avoidImageBrake(boolean value)
    {
        updateParameterMap(Constants.NO_IMG_BRAKE, value);
    }

    /**
     * Sets if to enable hyper links on the resultant PDF document.
     * 
     * @param value
     *            Boolean value. Default is false.
     */

    public void enableLinks(boolean value)
    {
        updateParameterMap(Constants.LIVE_LINKS, value);
    }

    /**
     * Sets the format of the document pages.
     * 
     * @param format
     *            The format of the document pages. The values are available in
     *            com.runpdf.Constants (Example: Constants.FORMAT_A0;). Default
     *            format is FORMAT_A4 - "A4".
     */

    public void setFormat(String format)
    {
        updateParameterMap(Constants.FORMAT, format);
    }

    /**
     * Sets the orientation of the document pages.
     * 
     * @param orientation
     *            The orientation of the document pages. The values are
     *            available in com.runpdf.Constants (Example:
     *            Constants.ORIENTATION_LANDSCAPE;). Default format is
     *            Constants.ORIENTATION_PORTRAIT.
     */

    public void setOrientation(String orientation)
    {
        updateParameterMap(Constants.ORIENTATION, orientation);
    }

    /**
     * Sets the HTTP source for the header.
     * 
     * @param headerSourceHtml
     *            HTTP source for the header.
     */

    public void setHeaderSourceHtml(String headerSourceHtml)
    {
        updateParameterMap(Constants.HEADER_SOURCE_HTML, headerSourceHtml);
    }

    /**
     * Sets the HTTP source for the footer.
     * 
     * @param footerSourceHtml
     *            HTTP source for the footer.
     */

    public void setFooterSourceHtml(String footerSourceHtml)
    {
        updateParameterMap(Constants.FOOTER_SOURCE_HTML, footerSourceHtml);
    }

    /**
     * Sets the URL source for the footer.
     * 
     * @param value
     *            URL source for the footer.
     */

    public void setFooterSourceUrl(String value)
    {
        updateParameterMap(Constants.FOOTER_SOURCE_URL, value);
    }

    /**
     * Sets the URL source for the header.
     * 
     * @param headerSourceUrl
     *            URL source for the header.
     */

    public void setHeaderSourceUrl(String headerSourceUrl)
    {
        updateParameterMap(Constants.HEADER_SOURCE_URL, headerSourceUrl);
    }

    /**
     * Sets the header height.
     * 
     * @param headerHeight
     *            Header height.
     */

    public void setHeaderHeight(String headerHeight)
    {
        updateParameterMap(Constants.HEADER_HEIGHT, headerHeight);
    }

    /**
     * Sets the footer height.
     * 
     * @param footerHeight
     *            Footer height.
     */

    public void setFooterHeight(String footerHeight)
    {
        updateParameterMap(Constants.FOOTER_HEIGHT, footerHeight);
    }

    /**
     * Sets if to show page number on the footer.
     * 
     * @param value
     *            Boolean value. Default is false.
     */

    public void showPageNumberOnFooter(boolean value)
    {
        updateParameterMap(Constants.FOOTER_SHOW_PAGE_NUMS, value);
    }

    /**
     * Sets if to start contents of the second web page on new page of the
     * document.
     * 
     * @param value
     *            Boolean value. Default is false.
     */

    public void showSecondDocumentOnNewPage(boolean value)
    {
        updateParameterMap(Constants.MLTPL_START_NEW_PAGE, value);
    }

    /**
     * Sets password of the document owner.
     * 
     * @param ownerPassword
     *            Password of the document owner.
     */

    public void setOwnerPassword(String ownerPassword)
    {
        updateParameterMap(Constants.SEC_OWNER_PASSWORD, ownerPassword);
    }

    /**
     * Sets password of the document user.
     * 
     * @param userPassword
     *            Password of the document user.
     */

    public void setUserPassword(String userPassword)
    {
        updateParameterMap(Constants.SEC_USER_PASSWORD, userPassword);
    }

    /**
     * Sets if to allow the document user to print the document.
     * 
     * @param value
     *            Boolean value. Default is true.
     */

    public void allowPrint(boolean value)
    {
        updateParameterMap(Constants.SEC_ALLOW_PRINT, value);
    }

    /**
     * Sets if to allow the document user to edit the document.
     * 
     * @param value
     *            Boolean value. Default is true.
     */

    public void allowEdit(boolean value)
    {
        updateParameterMap(Constants.SEC_ALLOW_EDIT, value);
    }

    /**
     * Sets if to allow the document user to copy contents of the document.
     * 
     * @param value
     *            Boolean value. Default is true.
     */

    public void allowCopy(boolean value)
    {
        updateParameterMap(Constants.SEC_ALLOW_COPY, value);
    }

    /**
     * Sets URI to the watermark image.
     * 
     * @param watermarkImageUrl
     *            URI to the watermark image.
     */

    public void setWatermarkImageUrl(String watermarkImageUrl)
    {
        updateParameterMap(Constants.WM_IMAGE_URL, watermarkImageUrl);
    }

    /**
     * Sets the text of the watermark.
     * 
     * @param watermarkText
     *            The text of the watermark.
     */

    public void setWatermarkText(String watermarkText)
    {
        updateParameterMap(Constants.WM_TEXT, watermarkText);
    }

    /**
     * Sets the opacity of the watermark.
     * 
     * @param watermarkOpacity
     *            The opacity of the watermark.
     */

    public void setWatermarkOpacity(int watermarkOpacity)
    {
        updateParameterMap(Constants.WM_OPACITY, "" + watermarkOpacity);
    }

    /**
     * Sets the watermark text color.
     * 
     * @param watermarkTextColor
     *            Color of the watermark text. A valid name of the color.
     *            Default is Gray.
     */

    public void setWatermarkTextColor(String watermarkTextColor)
    {
        updateParameterMap(Constants.WM_TEXT_COLOR, watermarkTextColor);
    }

    /**
     * Sets the size of the watarmark text font.
     * 
     * @param watermarkTextFontSize
     *            Size of the watarmark text font. A number between 0 and 100.
     *            Default is 50.
     */

    public void setWatermarkTextFontSize(int watermarkTextFontSize)
    {
        updateParameterMap(Constants.WM_TEXT_FONT_SIZE, "" + watermarkTextFontSize);
    }

    /**
     * Sets the watermark text position.
     * 
     * @param watermarkTextPosition
     *            Position of the watermark. The values are available in
     *            com.runpdf.Constants (Example:
     *            Constants.WM_TEXT_POSITION_DIAGONAL;). Default format is
     *            Constants.WM_TEXT_POSITION_HORIZONTAL.
     */

    public void setWatermarkTextPosition(String watermarkTextPosition)
    {
        updateParameterMap(Constants.WM_TEXT_POSITION, watermarkTextPosition);
    }

    //++++++++++++++++++++++++++++
    // Private methods
    //++++++++++++++++++++++++++++

    private int copyStream(InputStream inputStream, OutputStream outputStream) throws Exception
    {
        int size = 0;
        try
        {
            int read = 0;
            byte[] bytes = new byte[8192];
            while ((read = inputStream.read(bytes)) != -1)
            {
                size += read;
                outputStream.write(bytes, 0, read);
            }
        }
        catch (Exception e)
        {
            throw new Exception(Constants.ERROR_SERVER_UNEXPECTED);
        }
        finally
        {
            if (inputStream != null)
            {
                try
                {
                    inputStream.close();
                }
                catch (IOException e)
                {
                    throw new Exception(Constants.ERROR_SERVER_UNEXPECTED);
                }
            }
        }

        return size;
    }

    private void setSourceUrl2(String value)
    {
        updateParameterMap(Constants.SOURCE_URL2, value);
    }

    private void setBaseUrl(String value)
    {
        updateParameterMap(Constants.SOURCE_BASE_URL, value);
    }

    private void setExportKey(String value)
    {
        updateParameterMap(Constants.EXPORT_KEY, value);
    }

    private void setAuthenticationUsername(String value)
    {
        updateParameterMap(Constants.AUTH_USERNAME, value);
    }

    private void setAuthenticationPassword(String value)
    {
        updateParameterMap(Constants.AUTH_PASSWORD, value);
    }

    private void setDeliveryStream()
    {
        updateParameterMap(Constants.DELIVERY_STREAM, "Y");
    }

    private String getParameters()
    {
        if (parameterMap == null || parameterMap.isEmpty())
        {
            return null;
        }

        String exportKey = parameterMap.get(Constants.EXPORT_KEY);
        if (exportKey == null || exportKey.trim().length() < 3)
        {
            return null;
        }

        String sourceUrl = parameterMap.get(Constants.SOURCE_URL);
        String sourceHtml = parameterMap.get(Constants.SOURCE_HTML);
        if ((sourceUrl != null && sourceUrl.trim().length() > 0) || (sourceHtml != null && sourceHtml.trim().length() > 0))
        {
            String parameters;
            try
            {
                parameters = "";
                String dlmtr = "";
                for (Map.Entry<String, String> item : parameterMap.entrySet())
                {
                    parameters += dlmtr + item.getKey() + "=" + URLEncoder.encode(StringEscapeUtils.escapeHtml(item.getValue()), "UTF-8");
                    dlmtr = "&";
                }

                return parameters;
            }
            catch (Exception e)
            {
                return null;
            }
        }
        else
        {
            return null;
        }
    }

    private void updateParameterMap(String paramKey, boolean paramValue)
    {
        updateParameterMap(paramKey, getBooleanParamValue(paramValue));
    }

    private void updateParameterMap(String paramKey, String paramValue)
    {
        if (paramValue != null && paramValue.trim().length() > 0)
        {
            if (parameterMap == null)
            {
                parameterMap = new HashMap<String, String>();
            }
            parameterMap.put(paramKey, paramValue);
        }
    }

    private String getBooleanParamValue(boolean paramValue)
    {
        if (paramValue)
        {
            return Constants.BOOLEAN_TRUE;
        }

        return Constants.BOOLEAN_FALSE;
    }

}