/**
 *
 */
package com.softcorporation.util.net;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.net.UnknownHostException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.AuthenticationException;
import org.apache.http.auth.NTCredentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.auth.NTLMScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.softcorporation.util.StringUtil;

/**
 * HttpClient provides communication channel with remote HTTP server using Apache HTTP Client
 * 
 * @author Vadim Permakoff
 * 
 */
public class HttpClient {

    /**
     * LOGGER Logger.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(HttpClient.class);

    /**
     * Auth NTLM.
     */
    public static final String AUTH_TYPE_NTLM = "NTLM";
    
    /**
     * Auth BASIC.
     */
    public static final String AUTH_TYPE_BASIC = "BASIC";

    /**
     * Auth KERBEROS.
     */
    public static final String AUTH_TYPE_KERBEROS = "KERBEROS";

    /**
     * Headers list
     */
    private final List<Header> headers = new ArrayList<>();

    /**
     * Default buffer sizet
     */
    public static final int BUFFER_SIZE_DEFAULT = 8192;

    /**
     * Default http port
     */
    public static final int DEFAULT_HTTP_PORT = 80;

    /**
     * Default https port
     */
    public static final int DEFAULT_HTTPS_PORT = 443;

    /**
     * Default max buffer length
     */
    public static final long MAX_BUFFER_LENGTH = 100000000; // 100M

    /**
     * Default max file length
     */
    public static final long MAX_FILE_LENGTH = 1000000000;  // 1G

    /**
     * Connection Timeout Default
     */
    public static final int CONN_TIMEOUT_DEFAULT = 30000;

    /**
     * Request Timeout Default
     */
    public static final int TIMEOUT_DEFAULT = 200000;

    /**
     * Default proxy port.
     */
    public static final int PROXY_PORT_DEFAULT = 8080;

    /**
     * Connection timeout. Time in milliseconds allowed to establish connection
     * to remote server.
     */
    private int connTimeout = CONN_TIMEOUT_DEFAULT;

    /**
     * Request Timeout. Time in milliseconds allowed for request execution. Client
     * implementation code should return after this time expires.
     */
    private int timeout = TIMEOUT_DEFAULT;

    /**
     * Authentication type
     */
    private String authType;

    /**
     * Host name
     */
    private String hostName;

    /**
     * Domain
     */
    private String domain;

    /**
     * Remote service authorization user name.
     */
    private String authName;

    /**
     * Remote service authorization user password.
     */
    private String authPass;

    /**
     * Service proxy host.
     */
    private String proxyHost;

    /**
     * Service proxy port.
     */
    private int proxyPort = PROXY_PORT_DEFAULT;

    /**
     * Service proxy authorization user name.
     */
    private String proxyAuthName;

    /**
     * Service proxy authorization user password.
     */
    private String proxyAuthPass;

    /**
     * Default constructor
     * Initializes hostName and domain needed for NTLM.
     */
    public HttpClient() {
        try {
            InetAddress inetAddress = InetAddress.getLocalHost();
            hostName = inetAddress.getHostName();
            String canonicalHostName = inetAddress.getCanonicalHostName();
            if (canonicalHostName.startsWith(hostName + ".")) {
                domain = canonicalHostName.substring(hostName.length() + 1);
            } else {
                domain = "";
            }
        } catch (UnknownHostException e) {
            LOGGER.warn("Cannot get host name initializing HttpClient: " + e);
            hostName = "localhost";
            domain = "";
        }
    }

    /**
     * Returns timeout
     * @return timeout
     */
    public int getTimeout() {
        return timeout;
    }

    /**
     * Sets timeout
     * @param timeout int
     */
    public void setTimeout(final int timeout) {
        this.timeout = timeout;
    }

    /**
     * Returns connection timeout
     * @return connection timeout
     */
    public int getConnTimeout() {
        return connTimeout;
    }

    /**
     * Sets connection timeout
     * @param connTimeout connection timeout int
     */
    public void setConnTimeout(final int connTimeout) {
        this.connTimeout = connTimeout;
    }

    /**
     * Sets authType
     * @param authType String
     */
    public void setAuthType(final String authType) {
        this.authType = authType;
    }

    /**
     * Returns authType
     * @return authType
     */
    public String getAuthName() {
        return authName;
    }

    /**
     * Sets authName
     * @param authName String
     */
    public void setAuthName(final String authName) {
        this.authName = authName;
    }

    /**
     * Returns auth password
     * @return auth password
     */
    public String getAuthPass() {
        return authPass;
    }

    /**
     * Sets auth password
     * @param authPass The auth password String
     */
    public void setAuthPass(final String authPass) {
        this.authPass = authPass;
    }

    /**
     * Returns proxy host
     * @return proxy host
     */
    public String getProxyHost() {
        return proxyHost;
    }

    /**
     * Sets proxy host
     * @param proxyHost The proxy host String
     */
    public void setProxyHost(final String proxyHost) {
        this.proxyHost = proxyHost;
    }

    /**
     * Returns proxy port
     * @return proxy port
     */
    public int getProxyPort() {
        return proxyPort;
    }

    /**
     * Sets proxy port
     * @param proxyPort int
     */
    public void setProxyPort(final int proxyPort) {
        this.proxyPort = proxyPort;
    }

    /**
     * Returns proxy auth name
     * @return proxy auth name
     */
    public String getProxyAuthName() {
        return proxyAuthName;
    }

    /**
     * Sets proxy auth name
     * @param proxyAuthName The proxy auth name
     */
    public void setProxyAuthName(final String proxyAuthName) {
        this.proxyAuthName = proxyAuthName;
    }

    /**
     * Returns proxy auth password
     * @return proxy auth password
     */
    public String getProxyAuthPass() {
        return proxyAuthPass;
    }

    /**
     * Sets proxy auth password
     * @param proxyAuthPass The proxy auth password
     */
    public void setProxyAuthPass(final String proxyAuthPass) {
        this.proxyAuthPass = proxyAuthPass;
    }

    /**
     * Returns a list of HTTP Client's Header objects.
     * @return List
     */
    public List<Header> getHeaders() {
        return headers;
    }

    /**
     * Sets a single HTTP Client's Header object in the headers list.
     * @param name Header name.
     * @param value Header value.
     */
    public void addHeader(final String name, final String value) {
        Header header = new BasicHeader(name, value);
        headers.add(header);
    }

    /**
     * Get document.
     * 
     * @param clientRequest
     *            The request.
     * @return The response.
     * @throws IOException exception
     */
    public ClientResponse get(final ClientRequest clientRequest) throws IOException {
        String url = clientRequest.getUrl();
        if (url == null) {
            throw new IOException("HttpClient.get() The URL is null");
        }
        ClientResponse clientResponse = new ClientResponse(clientRequest);
        File file = clientRequest.getFile();
        if (file == null) {
            get(url, clientRequest.getEncoding(), clientResponse);
        } else {
            get(url, file, clientResponse);
        }
        return clientResponse;
    }

    /**
     * Post document.
     * 
     * @param clientRequest
     *            The request.
     * @return The response.
     *
     * @throws IOException exception
     */
    public ClientResponse post(final ClientRequest clientRequest) throws IOException {
        String url = clientRequest.getUrl();
        if (url == null) {
            throw new IOException("HttpClient.post() The URL is null");
        }

        ClientResponse clientResponse = new ClientResponse(clientRequest);
        File file = clientRequest.getFile();
        if (file == null) {
            post(url, clientRequest.getContent(), clientRequest.getEncoding(), clientResponse);
        } else {
            post(url, clientRequest.getContent(), file, clientResponse);
        }
        return clientResponse;
    }

    /**
     * Gets document and streams it to the file.
     * 
     * @param url
     *            The url of the remote file.
     * @param file
     *            The output data file.
     * @param clientResponse
     *            The client response
     *
     * @throws IOException exception
     */
    private void get(final String url, final File file, final ClientResponse clientResponse) throws IOException {
        if (url == null) {
            throw new IOException("HttpClient.get() The URL is null");
        }

        RequestConfig config = RequestConfig.custom().setConnectTimeout(connTimeout)
                .setConnectionRequestTimeout(timeout).setSocketTimeout(timeout).build();

        CloseableHttpClient client = HttpClientBuilder.create().setDefaultRequestConfig(config)
                .setDefaultHeaders(headers).build();

        HttpGet httpGet = null;
        try {
            URL targetUrl = new URL(url);

            httpGet = new HttpGet(url);
            HttpClientContext context = HttpClientContext.create();

            if (authName != null && authName.length() > 0) {
                HttpHost targetHost = new HttpHost(targetUrl.getHost(), targetUrl.getPort(), targetUrl.getProtocol());
                CredentialsProvider credsProvider = new BasicCredentialsProvider();

                if (AUTH_TYPE_BASIC.equals(authType)) {
                    credsProvider.setCredentials(new AuthScope(targetHost.getHostName(), targetHost.getPort()),
                            new UsernamePasswordCredentials(authName, authPass));

                    // Create AuthCache instance
                    AuthCache authCache = new BasicAuthCache();
                    // Generate BASIC scheme object and add it to the local auth cache
                    BasicScheme basicAuth = new BasicScheme();
                    authCache.put(targetHost, basicAuth);

                    // Add AuthCache to the execution context
                    context.setCredentialsProvider(credsProvider);
                    context.setAuthCache(authCache);
                } else if (AUTH_TYPE_NTLM.equals(authType)) {
                    credsProvider.setCredentials(AuthScope.ANY, new NTCredentials(authName, authPass, "", ""));

                    // Create AuthCache instance TODO
                    //                    AuthCache authCache = new BasicAuthCache();
                    //                    NTLMScheme scheme = new NTLMScheme();
                    //                    authCache.put(targetHost, scheme);

                    // Add AuthCache to the execution context
                    context.setCredentialsProvider(credsProvider);
                    //                    context.setAuthCache(authCache);
                } else if (AUTH_TYPE_KERBEROS.equals(authType)) {
                    // not implemented
                    throw new IOException("Kerberos Authentication not implemented in current version!");
                }
            }

            CloseableHttpResponse response = client.execute(httpGet, context);
            final HttpEntity entity = response.getEntity();

            if (entity == null) {
                LOGGER.error("Unable to download file from url " + url);
                throw new IOException("Unable to download file from url " + url);
            }

            final int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode != HttpStatus.SC_OK) {
                LOGGER.error("Unable to download file from url " + url + " statusCode:" + statusCode);
                throw new IOException("Cannot download file from url " + url + " HTTP Code: " + statusCode);
            }
            InputStream stream = entity.getContent();

            long received = 0;
            BufferedInputStream bis = new BufferedInputStream(stream);
            FileOutputStream fos = new FileOutputStream(file);
            byte[] bytes = new byte[BUFFER_SIZE_DEFAULT];
            int count = bis.read(bytes);
            while (count != -1 && count <= BUFFER_SIZE_DEFAULT) {
                fos.write(bytes, 0, count);
                received += count;
                count = bis.read(bytes);
            }
            if (count != -1) {
                fos.write(bytes, 0, count);
                received += count;
            }
            fos.close();
            bis.close();

            clientResponse.setStatusCode(statusCode);
            clientResponse.setContentLength(received);

        } catch (MalformedURLException e) {
            throw new IOException("Invalid get url " + e);
        } catch (ProtocolException e) {
            throw new IOException("Connection Exception " + e);
        } catch (IOException e) {
            throw new IOException("IOException " + e + " downloading file from url " + url);
        } finally {
        	if(null != httpGet) {
                httpGet.releaseConnection();        		
        	}
        }

        try {
            client.close();
        } catch (IOException e) {
            LOGGER.error("Error: " + e + " Unable to close client to url " + url);
        }
    }

    /**
     * Gets document and returns it as a string.
     * 
     * @param url
     *            The url of the remote file.
     * @param encoding
     *            The output data encoding.
     * @param clientResponse
     *            The client response
     *            
     * @throws IOException exception
     */
    private void get(final String url, final String encoding, final ClientResponse clientResponse) throws IOException {
        if (url == null) {
            throw new IOException("HttpClient.get() The URL is null");
        }
        StringBuilder output = new StringBuilder();
        String enc;
        if (encoding == null) {
            enc = StringUtil.ENCODING_DEFAULT;
        } else { 
            enc = encoding;
        }

        RequestConfig config = RequestConfig.custom().setConnectTimeout(connTimeout)
                .setConnectionRequestTimeout(timeout).setSocketTimeout(timeout).build();

        CloseableHttpClient client = HttpClientBuilder.create().setDefaultRequestConfig(config)
                .setDefaultHeaders(headers).build();

        HttpGet httpGet = null;
        try {
            //            final CredentialsProvider credsProvider = new BasicCredentialsProvider();
            //            credsProvider.setCredentials(new AuthScope(AuthScope.ANY), new NTCredentials(authName, authPass, "", ""));
            //            final CloseableHttpClient httpclient = HttpClients.custom().setDefaultCredentialsProvider(credsProvider).build();

            URL targetUrl = new URL(url);

            httpGet = new HttpGet(url);
            HttpClientContext context = HttpClientContext.create();

            if (authName != null && authName.length() > 0) {
                //                UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(authName, authPass);
                //                httpGet.addHeader(new BasicScheme().authenticate(credentials, httpGet, null));

                HttpHost targetHost = new HttpHost(targetUrl.getHost(), targetUrl.getPort(), targetUrl.getProtocol());
                CredentialsProvider credsProvider = new BasicCredentialsProvider();

                if (AUTH_TYPE_BASIC.equals(authType)) {
                    credsProvider.setCredentials(new AuthScope(targetHost.getHostName(), targetHost.getPort()),
                            new UsernamePasswordCredentials(authName, authPass));

                    // Create AuthCache instance
                    AuthCache authCache = new BasicAuthCache();
                    // Generate BASIC scheme object and add it to the local auth cache
                    BasicScheme basicAuth = new BasicScheme();
                    authCache.put(targetHost, basicAuth);

                    // Add AuthCache to the execution context
                    context.setCredentialsProvider(credsProvider);
                    context.setAuthCache(authCache);
                } else if (AUTH_TYPE_NTLM.equals(authType)) {

                    credsProvider.setCredentials(AuthScope.ANY,
                            new NTCredentials(authName, authPass, hostName, domain));

                    // Create AuthCache instance
                    AuthCache authCache = new BasicAuthCache();
                    NTLMScheme scheme = new NTLMScheme();
                    authCache.put(targetHost, scheme);

                    // Add AuthCache to the execution context
                    context.setCredentialsProvider(credsProvider);
                    context.setAuthCache(authCache);
                } else if (AUTH_TYPE_KERBEROS.equals(authType)) {
                    // not implemented
                    throw new IOException("Kerberos Authentication is not implemented!");
                }
            }

            CloseableHttpResponse response = client.execute(httpGet, context);
            final HttpEntity entity = response.getEntity();

            if (entity == null) {
                LOGGER.error("Unable to download file from url " + url);
                throw new IOException("Unable to download file from url " + url);
            }

            final int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode != HttpStatus.SC_OK) {
                LOGGER.error("Unable to download file from url " + url + " statusCode:" + statusCode);
                throw new IOException("Unable to download file from url " + url + " HTTP Code: " + statusCode);
            }

            InputStream stream = entity.getContent();
            InputStreamReader reader = new InputStreamReader(stream, enc);

            int data = reader.read();
            while (data >= 0) {
                char chr = (char) data;
                output.append(chr);
                if (output.length() > MAX_BUFFER_LENGTH) {
                    throw new IOException("Too long input from URL " + url);
                }
                data = reader.read();
            }
            reader.close();
            
            String received = output.toString();
            clientResponse.setContent(received);
            clientResponse.setContentLength(received.length());

            // calculate md5
            if (clientResponse.getClientRequest().isCalcChksum()) {
                clientResponse.setChksum(StringUtil.getMD5(clientResponse.getContent()));
            }
            clientResponse.setStatusCode(statusCode);

        } catch (MalformedURLException e) {
            throw new IOException("Invalid get url " + e);
        } catch (ProtocolException e) {
            throw new IOException("Connection Exception " + e);
        } catch (IOException e) {
            throw new IOException("IOException " + e + " downloading file from url " + url);
        } catch (NoSuchAlgorithmException e) {
            throw new IOException("MD5 error: " + e);
        } finally {
        	if(null != httpGet) {
        		httpGet.releaseConnection();
        	}
        }
        try {
            client.close();
        } catch (IOException e) {
            LOGGER.error("Error: " + e + " Unable to close client to url " + url);
        }
    }

    /**
     * Post request and stream response to a file.
     * 
     * @param url
     *            The url of the remote file.
     * @param content
     *            The output content.
     * @param file
     *            The output data file.
     * @param clientResponse
     *            The client response
     * @throws IOException exception
     */
    private void post(final String url, final String content, final File file, final ClientResponse clientResponse) throws IOException {
        if (url == null) {
            throw new IOException("HttpClient.get() The URL is null");
        }

        RequestConfig config = RequestConfig.custom().setConnectTimeout(connTimeout)
                .setConnectionRequestTimeout(timeout).setSocketTimeout(timeout).build();

        CloseableHttpClient client = HttpClientBuilder.create().setDefaultRequestConfig(config)
                .setDefaultHeaders(headers).build();

        HttpPost httpPost = null;
        try {
            httpPost = new HttpPost(url);

            httpPost.setEntity(new StringEntity(content));

            if (authName != null && authName.length() > 0) {
                UsernamePasswordCredentials creds = new UsernamePasswordCredentials(authName, authPass);
                httpPost.addHeader(new BasicScheme().authenticate(creds, httpPost, null));
            }

            CloseableHttpResponse response = client.execute(httpPost);
            final HttpEntity entity = response.getEntity();

            if (entity == null) {
                LOGGER.error("Unable to download file from url " + url);
                throw new IOException("Unable to download file from url " + url);
            }

            final int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode != HttpStatus.SC_OK) {
                LOGGER.error("Unable to download file from url " + url + " statusCode:" + statusCode);
                throw new IOException("Unable to download file from url " + url + " HTTP Code: " + statusCode);
            }

            InputStream stream = entity.getContent();

            long received = 0;
            BufferedInputStream bis = new BufferedInputStream(stream);
            FileOutputStream fos = new FileOutputStream(file);
            byte[] bytes = new byte[BUFFER_SIZE_DEFAULT];
            int count = bis.read(bytes);
            while (count != -1 && count <= BUFFER_SIZE_DEFAULT) {
                fos.write(bytes, 0, count);
                received += count;
                count = bis.read(bytes);
            }
            if (count != -1) {
                fos.write(bytes, 0, count);
                received += count;
            }
            fos.close();
            bis.close();

            clientResponse.setStatusCode(statusCode);
            clientResponse.setContentLength(received);
            
        } catch (MalformedURLException e) {
            throw new IOException("Invalid post url " + e);
        } catch (AuthenticationException e) {
            throw new IOException("Invalid credentials " + e);
        } catch (ProtocolException e) {
            throw new IOException("Connection Exception " + e);
        } catch (IOException e) {
            throw new IOException("IOException " + e + " downloading file from url " + url);
        } finally {
        	if(null != httpPost) {
        		httpPost.releaseConnection();
        	}
        }

        try {
            client.close();
        } catch (IOException e) {
            LOGGER.error("Error: " + e + " Unable to close client to url " + url);
        }
    }

    /**
     * Post request and return response as a string.
     * 
     * @param url
     *            The url of the remote file.
     * @param content
     *            The output content
     * @param encoding
     *            The output data encoding.
     * @param clientResponse
     *            The client response
     * @throws IOException exception
     */
    private void post(final String url, final String content, final String encoding, final ClientResponse clientResponse)
            throws IOException {
        if (url == null) {
            throw new IOException("HttpClient.post() The URL is null");
        }
        StringBuilder output = new StringBuilder();
        String enc;
        if (encoding == null) {
            enc = StringUtil.ENCODING_DEFAULT;
        } else { 
            enc = encoding;
        }

        RequestConfig config = RequestConfig.custom().setConnectTimeout(connTimeout)
                .setConnectionRequestTimeout(timeout).setSocketTimeout(timeout).build();

        CloseableHttpClient client = HttpClientBuilder.create().setDefaultRequestConfig(config)
                .setDefaultHeaders(headers).build();

        HttpPost httpPost = null;
        try {
            httpPost = new HttpPost(url);
//            httpPost.setProtocolVersion(HttpVersion.HTTP_1_0);
            httpPost.setEntity(new StringEntity(content, enc));

            if (authName != null && authName.length() > 0) {
                UsernamePasswordCredentials creds = new UsernamePasswordCredentials(authName, authPass);
                httpPost.addHeader(new BasicScheme().authenticate(creds, httpPost, null));
            }

            CloseableHttpResponse response = client.execute(httpPost);
            final HttpEntity entity = response.getEntity();

            if (entity == null) {
                LOGGER.error("Unable to download file from url : " + url);
                throw new IOException("Unable to download file from url : " + url);
            }

            final int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode != HttpStatus.SC_OK) {
                LOGGER.error("Unable to download file from url " + url + " statusCode:" + statusCode);
                throw new IOException("Unable to download file from url " + url + " HTTP Code: " + statusCode);
            }

            InputStream stream = entity.getContent();
            InputStreamReader reader = new InputStreamReader(stream, enc);

            int data = reader.read();
            while (data >= 0) {
                char chr = (char) data;
                output.append(chr);
                if (output.length() > MAX_BUFFER_LENGTH) {
                    throw new IOException("Too long input from URL " + url);
                }
                data = reader.read();
            }
            reader.close();

            String received = output.toString();
            clientResponse.setContent(received);
            clientResponse.setContentLength(received.length());

            // calculate md5
            if (clientResponse.getClientRequest().isCalcChksum()) {
                clientResponse.setChksum(StringUtil.getMD5(clientResponse.getContent()));
            }
            clientResponse.setStatusCode(statusCode);

        } catch (MalformedURLException e) {
            throw new IOException("Invalid post url " + e);
        } catch (AuthenticationException e) {
            throw new IOException("Invalid credentials " + e);
        } catch (ProtocolException e) {
            throw new IOException("Connection Exception " + e);
        } catch (IOException e) {
            throw new IOException("IOException " + e + " downloading file from url " + url);
        } catch (NoSuchAlgorithmException e) {
            throw new IOException("MD5 error: " + e);
        } finally {
        	if(null != httpPost) {
        		httpPost.releaseConnection();
        	}
        }
        try {
            client.close();
        } catch (IOException e) {
            LOGGER.error("Error: " + e + " Unable to close client to url " + url);
        }
    }

}
