/// 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.

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Web;

namespace RunPDF
{
    public class ExportClient
    {
        private Dictionary<stringstring> parameterMap = new Dictionary<stringstring>();
        private string documentName = Constants.DOC_NAME;
        private string serviceURL = Constants.SERVICE_URL;

        /// <summary>
        /// Client constructor.
        /// </summary>
        /// <param name="exportKey"> 
        /// A unique key assigned to all registered accounts for request authentication.
        /// </param>
        public ExportClient(string exportKey)
        {
            setDeliveryStream();
            setExportKey(exportKey);
        }

        /// <summary>
        /// Client constructor.
        /// </summary>
        /// <param name="exportKey"> 
        /// A unique key assigned to all registered accounts for request authentication.
        /// </param>
        /// <param name="serviceURL"> 
        /// The URL to RunPDF export web service.
        /// </param>
        public ExportClient(string exportKey, string serviceURL)
        {
            this.serviceURL = serviceURL;
            setDeliveryStream();
            setExportKey(exportKey);
        }

        /// <summary>
        /// Exports HTML content to PDF and copies the stream of the resultant document to 
        /// the target OutputStream.
        /// </summary>
        /// <param name="outputStream"> 
        /// The target OutputStream.
        /// </param>
        /// <returns>
        /// Size of the resultant document stream.
        /// </returns>
        /// <exception cref="System.Exception">
        /// The exception is thrown in case of failure to execute. The exception message is 
        /// set to a specific error code 
        /// (see RunPDF.Constants.ERROR_CODE_MESSAGE_MAP).
        /// </exception>
        public int export(Stream outputStream)
        {
            string parameters = getParameters();
            if (parameters == null)
            {
                throw new Exception(Constants.ERROR_INPUT);
            }

            byte[] byteArray = Encoding.UTF8.GetBytes((string)parameters);
            WebRequest request = WebRequest.Create(serviceURL);
            request.Method = "POST";
            request.ContentType = "application/x-www-form-urlencoded";
            request.ContentLength = byteArray.Length;

            Stream dataStream = request.GetRequestStream();
            dataStream.Write(byteArray, 0, byteArray.Length);
            dataStream.Close();

            using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
            {
                HttpStatusCode statusCode = response.StatusCode;
                if (statusCode == HttpStatusCode.OK)
                {
                    using (var inputStream = response.GetResponseStream())
                    {
                        return copyStream(inputStream, outputStream);
                    }
                }
                else
                {
                    throw new Exception("" + (int)statusCode);
                }
            }
        }

        /// <summary>
        /// Exports HTML content to PDF and saves the document to the disk.
        /// </summary>
        /// <param name="response">Current HttpResponse.</param>
        /// <returns>
        /// null if export is successful, otherwise a message that corresponds to the code of the error
        /// (see RunPDF.Constants.ERROR_CODE_MESSAGE_MAP)
        /// </returns>
        public String exportToAttachment(HttpResponse response)
        {
            try
            {
                exportOnline(response, false);
                return null;
            }
            catch (Exception e)
            {
                return e.Message;
            }
        }

        /// <summary>
        ///Exports HTML content to PDF and opens the document in the browser.
        /// </summary>
        /// <param name="response">Current HttpResponse.</param>
        /// null if export is successful, otherwise a message that corresponds to the code of the error
        /// (see RunPDF.Constants.ERROR_CODE_MESSAGE_MAP)
        /// </returns>
        public String exportInline(HttpResponse response)
        {
            try
            {
                exportOnline(response, true);
                return null;
            }
            catch (Exception e)
            {
                return getErrorMessage(e.Message);
            }
        }

        /// <summary>
        ///Exports HTML content to the specified PDF file.
        /// </summary>
        /// <param name="filePath">A path to the target file.</param>
        /// <returns>
        /// null if export is successful, otherwise a message that corresponds to the code of the error
        /// (see RunPDF.Constants.ERROR_CODE_MESSAGE_MAP)
        /// </returns>
        public String exportToFile(string filePath)
        {
            try
            {
                using (var outputStream = File.Create(filePath))
                {
                    export(outputStream);
                }

                return null;
            }
            catch (Exception e)
            {
                return getErrorMessage(e.Message);
            }
        }

        /// <summary>
        /// Returns an error message based on the specified error code.
        /// </summary>
        /// <param name="errorCode">The error code.</param>
        /// A message that corresponds to the code of the error (see RunPDF.Constants.ERROR_CODE_MESSAGE_MAP)
        /// </returns>
        public String getErrorMessage(String errorCode)
        {
            for (int row = 0; row < Constants.ERROR_CODE_MESSAGE_MAP.GetLength(0); row++)
            {
                if (Constants.ERROR_CODE_MESSAGE_MAP[row, 0].Equals(errorCode))
                {
                    return Constants.ERROR_CODE_MESSAGE_MAP[row, 0];
                }
            }

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

        /// <summary>
        /// Sets the name of the resultant document.
        /// </summary>
        /// <param name="documentName">The name of the resultant document.</param>
        public void setDocumentName(string documentName)
        {
            if (documentName != null && documentName.Trim().Length > 0)
            {
                documentName = documentName.Trim();
                if (!documentName.ToLower().EndsWith(".pdf"))
                {
                    documentName += ".pdf";
                }
                this.documentName = documentName;
            }
        }

        /// <summary>
        ///Sets source web page URL to convert.
        /// </summary>
        /// <param name="sourceUrl">URL of the web page to convert.</param>
        public void setSourceUrl(string sourceUrl)
        {
            updateParameterMap(Constants.SOURCE_URL, sourceUrl);
        }

        /// <summary>
        ///Sets URLs of the primary and secondary web pages to convert.
        /// </summary>
        /// <param name="sourceUrl">URL of the primary web page to convert.</param>
        /// <param name="sourceUrl2">URL of the secondary web page to convert.</param>
        public void setSourceUrl(string sourceUrl, string sourceUrl2)
        {
            setSourceUrl(sourceUrl);
            setSourceUrl2(sourceUrl2);
        }

        /// <summary>
        ///Sets HTML source string.
        /// </summary>
        /// <param name="sourceHtml">HTML string to convert.</param>
        public void setSourceHtml(string sourceHtml)
        {
            updateParameterMap(Constants.SOURCE_HTML, sourceHtml);
        }

        /// <summary>
        ///Sets HTML source string and the base URL.
        /// </summary>
        /// <param name="sourceHtml">HTML string to convert.</param>
        /// <param name="baseUrl">URL of the web page to process CSS when converting HTML
        ///source.</param>
        public void setSourceHtml(string sourceHtml, string baseUrl)
        {
            setSourceHtml(sourceHtml);
            setBaseUrl(baseUrl);
        }

        /// <summary>
        ///Sets username and password credentials for NTLM authentication. Note:
        ///only NTLM authentication is supported.
        /// </summary>
        /// <param name="username">The username credential.</param>
        /// <param name="password">The password credential.</param>
        public void setAuthentication(string username, string password)
        {
            setAuthenticationUsername(username);
            setAuthenticationPassword(password);
        }

        /// <summary>
        ///Set if to present the document on media type print styles. 
        /// </summary>
        /// <param name="value">Boolean value. Default is false.</param>
        public void setMediaTypePrint(bool value)
        {
            updateParameterMap(Constants.MEDIA_TYPE_PRINT, standard); 
        }

        /// <summary>
        ///Sets the standard of the resultant PDF document.
        /// </summary>
        /// <param name="standard">
        ///The standard of the resultant PDF document. The values are available in
        ///RunPDF.Constants (Example: Constants.STANDARD_PDF_A;). Default format is STANDARD_PDF.
        /// </param>
        public void setStandard(string standard)
        {
            updateParameterMap(Constants.STANDARD, standard);
        }

        /// <summary>
        ///Sets if to avoid image break between pages.
        /// </summary>
        /// <param name="value">Boolean value. Default is false.</param>
        public void avoidImageBrake(bool value)
        {
            updateParameterMap(Constants.NO_IMG_BRAKE, value);
        }

        /// <summary>
        ///Sets if to enable hyper links on the resultant PDF document.
        /// </summary>
        /// <param name="value">Boolean value. Default is false.</param>
        public void enableLinks(bool value)
        {
            updateParameterMap(Constants.LIVE_LINKS, value);
        }

        /// <summary>
        ///Sets the format of the document pages.
        /// </summary>
        /// <param name="format">
        ///The format of the document pages. The values are available in
        ///RunPDF.Constants (Example: Constants.FORMAT_A0;). Default
        ///format is FORMAT_A4 - "A4".
        /// </param>
        public void setFormat(string format)
        {
            updateParameterMap(Constants.FORMAT, format);
        }

        /// <summary>
        ///Sets the orientation of the document pages.
        /// </summary>
        /// <param name="orientation">
        ///The orientation of the document pages. The values are
        ///available in RunPDF.Constants (Example: Constants.ORIENTATION_LANDSCAPE;). 
        ///Default format is Constants.ORIENTATION_PORTRAIT.
        /// </param>
        public void setOrientation(string orientation)
        {
            updateParameterMap(Constants.ORIENTATION, orientation);
        }

        /// <summary>
        ///Sets the HTTP source for the header.
        /// </summary>
        /// <param name="headerSourceHtml">HTTP source for the header.</param>
        public void setHeaderSourceHtml(string headerSourceHtml)
        {
            updateParameterMap(Constants.HEADER_SOURCE_HTML, headerSourceHtml);
        }

        /// <summary>
        ///Sets the HTTP source for the footer.
        /// </summary>
        /// <param name="footerSourceHtml">HTTP source for the footer.</param>
        public void setFooterSourceHtml(string footerSourceHtml)
        {
            updateParameterMap(Constants.FOOTER_SOURCE_HTML, footerSourceHtml);
        }

        /// <summary>
        ///Sets the URL source for the footer.
        /// </summary>
        /// <param name="value">URL source for the footer.</param>
        public void setFooterSourceUrl(string footerSourceUrl)
        {
            updateParameterMap(Constants.FOOTER_SOURCE_URL, footerSourceUrl);
        }

        /// <summary>
        ///Sets the URL source for the header.
        /// </summary>
        /// <param name="headerSourceUrl">URL source for the header.</param>
        public void setHeaderSourceUrl(string headerSourceUrl)
        {
            updateParameterMap(Constants.HEADER_SOURCE_URL, headerSourceUrl);
        }

        /// <summary>
        ///Sets the header height.
        /// </summary>
        /// <param name="headerHeight">Header height.</param>
        public void setHeaderHeight(string headerHeight)
        {
            updateParameterMap(Constants.HEADER_HEIGHT, headerHeight);
        }

        /// <summary>
        ///Sets the footer height.
        /// </summary>
        /// <param name="footerHeight">Footer height.</param>
        public void setFooterHeight(string footerHeight)
        {
            updateParameterMap(Constants.FOOTER_HEIGHT, footerHeight);
        }

        /// <summary>
        ///Sets if to show page number on the footer.
        /// </summary>
        /// <param name="value">Boolean value. Default is false.</param>
        public void showPageNumberOnFooter(bool value)
        {
            updateParameterMap(Constants.FOOTER_SHOW_PAGE_NUMS, value);
        }

        /// <summary>
        ///Sets if to start contents of the second web page on new page of the
        ///document.
        /// </summary>
        /// <param name="value">Boolean value. Default is false.</param>
        public void showSecondDocumentOnNewPage(bool value)
        {
            updateParameterMap(Constants.MLTPL_START_NEW_PAGE, value);
        }

        /// <summary>
        ///Sets password of the document owner.
        /// </summary>
        /// <param name="ownerPassword">Password of the document owner.</param>
        public void setOwnerPassword(string ownerPassword)
        {
            updateParameterMap(Constants.SEC_OWNER_PASSWORD, ownerPassword);
        }

        /// <summary>
        ///Sets password of the document user.
        /// </summary>
        /// <param name="userPassword">Password of the document user.</param>
        public void setUserPassword(string userPassword)
        {
            updateParameterMap(Constants.SEC_USER_PASSWORD, userPassword);
        }

        /// <summary>
        ///Sets if to allow the document user to print the document.
        /// </summary>
        /// <param name="value">Boolean value. Default is true.</param>
        public void allowPrint(bool value)
        {
            updateParameterMap(Constants.SEC_ALLOW_PRINT, value);
        }

        /// <summary>
        ///Sets if to allow the document user to edit the document.
        /// </summary>
        /// <param name="value">Boolean value. Default is true.</param>
        public void allowEdit(bool value)
        {
            updateParameterMap(Constants.SEC_ALLOW_EDIT, value);
        }

        /// <summary>
        ///Sets if to allow the document user to copy contents of the document.
        /// </summary>
        /// <param name="value">Boolean value. Default is true.</param>
        public void allowCopy(bool value)
        {
            updateParameterMap(Constants.SEC_ALLOW_COPY, value);
        }

        /// <summary>
        ///Sets URI to the watermark image.
        /// </summary>
        /// <param name="watermarkImageUrl">URI to the watermark image.</param>
        public void setWatermarkImageUrl(string watermarkImageUrl)
        {
            updateParameterMap(Constants.WM_IMAGE_URL, watermarkImageUrl);
        }

        /// <summary>
        ///Sets the text of the watermark.
        /// </summary>
        /// <param name="watermarkText">The text of the watermark.</param>
        public void setWatermarkText(string watermarkText)
        {
            updateParameterMap(Constants.WM_TEXT, watermarkText);
        }

        /// <summary>
        ///Sets the watermark opacity.
        /// </summary>
        /// <param name="watermarkOpacity">Opacity of the watarmark. 
        ///A number between 0 and 100. Default is 50.</param>
        public void setWatermarkOpacity(int watermarkOpacity)
        {
            updateParameterMap(Constants.WM_OPACITY, "" + watermarkOpacity);
        }

        /// <summary>
        ///Sets the watermark text color.
        /// </summary>
        /// <param name="watermarkTextColor">Color of the watermark text. 
        ///A valid name of the color. Default is Gray.</param>
        public void setWatermarkTextColor(string watermarkTextColor)
        {
            updateParameterMap(Constants.WM_TEXT_COLOR, watermarkTextColor);
        }

        /// <summary>
        ///Sets the size of the watarmark text font.
        /// </summary>
        /// <param name="watermarkTextFontSize">Size of the watarmark text font. 
        ///A number between 0 and 100. Default is 50.</param>
        public void setWatermarkTextFontSize(int watermarkTextFontSize)
        {
            updateParameterMap(Constants.WM_TEXT_FONT_SIZE, "" + watermarkTextFontSize);
        }

        /// <summary>
        ///Sets the watermark text position.
        /// </summary>
        /// <param name="watermarkTextPosition"> Position of the watermark. 
        ///The values are available in RunPDF.Constants (Example: Constants.WM_TEXT_POSITION_DIAGONAL;). 
        ///Default position is Constants.WM_TEXT_POSITION_HORIZONTAL.</param>
        public void setWatermarkTextPosition(string watermarkTextPosition)
        {
            updateParameterMap(Constants.WM_TEXT_POSITION, watermarkTextPosition);
        }


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

        private void exportOnline(HttpResponse httpResponse, bool isInline)
        {
            string delivery = (isInline) ? "inline" : "attachment";
            try
            {
                using (var outputStream = httpResponse.OutputStream)
                {
                    int documentSize = export(outputStream);
                    httpResponse.AddHeader("Content-Type""application/pdf; charset=utf-8");
                    httpResponse.AddHeader("Content-Disposition"string.Format(delivery + "; filename=" + getDocumentName() + "; size={0}", documentSize));
                    httpResponse.AddHeader("Set-Cookie""fileDownload=true; path=/");
                }
            }
            catch (Exception e)
            {
                throw new Exception(e.Message);
            }
        }

        private static int copyStream(Stream inputStream, Stream outputStream)
        {
            int size = 0;
            int read = 0;
            byte[] buffer = new byte[32768];
            while (true)
            {
                read = inputStream.Read(buffer, 0, buffer.Length);
                if (read <= 0)
                {
                    break;
                }

                size += read;
                outputStream.Write(buffer, 0, read);
            }

            return size;
        }

        private string getDocumentName()
        {
            return documentName;
        }

        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.Count < 1)
            {
                throw new Exception("Invalid input: export key and source are required.");
            }

            string exportKey;
            parameterMap.TryGetValue(Constants.EXPORT_KEY, out exportKey);
            if (string.IsNullOrEmpty(exportKey) || exportKey.Trim().Length < 3)
            {
                throw new Exception("Invalid input: invalid export key.");
            }

            string sourceUrl;
            string sourceHtml;
            parameterMap.TryGetValue(Constants.SOURCE_URL, out sourceUrl);
            parameterMap.TryGetValue(Constants.SOURCE_HTML, out sourceHtml);
            if (!string.IsNullOrEmpty(sourceUrl) || !string.IsNullOrEmpty(sourceHtml))
            {
                string parameters;
                try
                {
                    parameters = "";
                    string dlmtr = "";
                    foreach (KeyValuePair<stringstring> item in parameterMap)
                    {
                        string encodedValue = HttpContext.Current.Server.HtmlEncode(item.Value);
                        encodedValue = HttpUtility.UrlEncode(encodedValue);
                        parameters += dlmtr + item.Key + "=" + encodedValue;
                        dlmtr = "&";
                    }

                    return parameters;
                }
                catch (Exception e)
                {
                    throw new Exception("Invalid input: " + e.Message);
                }
            }
            else
            {
                throw new Exception("Invalid input: source is required.");
            }
        }

        private void updateParameterMap(string paramKey, bool paramValue)
        {
            updateParameterMap(paramKey, getboolParamValue(paramValue));
        }

        private void updateParameterMap(string paramKey, string paramValue)
        {
            if (!string.IsNullOrEmpty(paramValue))
            {
                if (parameterMap == null)
                {
                    parameterMap = new Dictionary<stringstring>();
                }
                parameterMap.Add(paramKey, paramValue);
            }
        }

        private string getboolParamValue(bool paramValue)
        {
            if (paramValue)
            {
                return Constants.BOOLEAN_TRUE;
            }

            return Constants.BOOLEAN_FALSE;
        }
    }
}