/*******************************************************************************
 * Copyright (c) 2011 Tasktop Technologies and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Tasktop Technologies - initial API and implementation
 *******************************************************************************/

package org.eclipse.mylyn.commons.repositories.http.core;

import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.Locale;

import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.NTCredentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.params.AuthPolicy;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.client.params.CookiePolicy;
import org.apache.http.client.params.HttpClientParams;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.scheme.SchemeSocketFactory;
import org.apache.http.impl.EnglishReasonPhraseCatalog;
import org.apache.http.impl.client.AbstractHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.eclipse.core.net.proxy.IProxyData;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.mylyn.commons.core.CoreUtil;
import org.eclipse.mylyn.commons.core.StatusHandler;
import org.eclipse.mylyn.commons.core.net.AuthenticatedProxy;
import org.eclipse.mylyn.commons.core.net.NetUtil;
import org.eclipse.mylyn.commons.core.operations.CancellableOperationMonitorThread;
import org.eclipse.mylyn.commons.core.operations.ICancellableOperation;
import org.eclipse.mylyn.commons.core.operations.IOperationMonitor;
import org.eclipse.mylyn.commons.core.operations.OperationUtil;
import org.eclipse.mylyn.commons.repositories.core.RepositoryLocation;
import org.eclipse.mylyn.commons.repositories.core.auth.AuthenticationType;
import org.eclipse.mylyn.commons.repositories.core.auth.UserCredentials;
import org.eclipse.mylyn.internal.commons.repositories.http.core.IdleConnectionMonitorThread;
import org.eclipse.mylyn.internal.commons.repositories.http.core.PollingProtocolSocketFactory;
import org.eclipse.mylyn.internal.commons.repositories.http.core.PollingSslProtocolSocketFactory;

/**
 * @author Steffen Pingel
 * @author Shawn Minto
 * @author Christian Janz
 * @noinstantiate This class is not intended to be instantiated by clients.
 */
public class HttpUtil {

	static {
		CoreUtil.initializeLoggingSettings();
	}

	@SuppressWarnings("unused")
	private static final int BUFFER_SIZE = 4096;

	@SuppressWarnings("unused")
	private static final long CLOSE_TIMEOUT = -1;

	/**
	 * @see IdleConnectionMonitorThread
	 */
	private static final int CONNECTION_TIMEOUT_INTERVAL = 1 * 30 * 1000;

	private static final int CONNNECT_TIMEOUT = 60 * 1000;

	private static final int HTTP_PORT = 80;

	private static final int HTTPS_PORT = 443;

	private static final int POLL_INTERVAL = 500;

	private static final int SOCKET_TIMEOUT = 3 * 60 * 1000;

	@SuppressWarnings("unused")
	private static final int POLL_ATTEMPTS = SOCKET_TIMEOUT / POLL_INTERVAL;

	private static SchemeSocketFactory socketFactory = new PollingProtocolSocketFactory();

	private static SchemeSocketFactory sslSocketFactory = new PollingSslProtocolSocketFactory();

	static final String ID_PLUGIN = "org.eclipse.mylyn.commons.repositories.http"; //$NON-NLS-1$

	private static ThreadSafeClientConnManager connectionManager;

	static final String CONTEXT_KEY_MONITOR_THREAD = CancellableOperationMonitorThread.class.getName();

	public static void configureClient(AbstractHttpClient client, String userAgent) {
		HttpClientParams.setCookiePolicy(client.getParams(), CookiePolicy.BEST_MATCH);

		HttpProtocolParams.setUserAgent(client.getParams(), userAgent);
		HttpProtocolParams.setUseExpectContinue(client.getParams(), true);
		client.getParams().setBooleanParameter(ClientPNames.ALLOW_CIRCULAR_REDIRECTS, true);

		HttpConnectionParams.setConnectionTimeout(client.getParams(), CONNNECT_TIMEOUT);
		HttpConnectionParams.setSoTimeout(client.getParams(), SOCKET_TIMEOUT);

		//AuthParams.setCredentialCharset(client.getParams(), "UTF-8");
	}

	public static void configureAuthentication(AbstractHttpClient client, RepositoryLocation location) {
		UserCredentials credentials = location.getCredentials(AuthenticationType.HTTP);
		if (credentials != null) {
			configureAuthentication(client, location, credentials);
		}
	}

	public static void configureAuthentication(AbstractHttpClient client, RepositoryLocation location,
			UserCredentials credentials) {
		Assert.isNotNull(client);
		Assert.isNotNull(location);
		Assert.isNotNull(credentials);
		String url = location.getUrl();
		Assert.isNotNull(url, "The location url must not be null"); //$NON-NLS-1$

		String host = NetUtil.getHost(url);
		int port = NetUtil.getPort(url);

		NTCredentials ntlmCredentials = getNtCredentials(credentials, ""); //$NON-NLS-1$
		if (ntlmCredentials != null) {
			AuthScope authScopeNtlm = new AuthScope(host, port, AuthScope.ANY_REALM, AuthPolicy.NTLM);
			client.getCredentialsProvider().setCredentials(authScopeNtlm, ntlmCredentials);
		}

		UsernamePasswordCredentials usernamePasswordCredentials = getUserNamePasswordCredentials(credentials);
		AuthScope authScopeAny = new AuthScope(host, port, AuthScope.ANY_REALM);
		client.getCredentialsProvider().setCredentials(authScopeAny, usernamePasswordCredentials);
	}

	public static HttpHost createHost(HttpRequestBase method) {
		URI uri = method.getURI();
		return new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme());
	}

	public static HttpResponse execute(final AbstractHttpClient client, final HttpContext context,
			final HttpRequestBase method, IProgressMonitor monitor) throws IOException {
		return execute(client, createHost(method), context, method, monitor);
	}

	public static HttpResponse execute(final AbstractHttpClient client, final HttpHost host, final HttpContext context,
			final HttpRequestBase method, final IProgressMonitor progress) throws IOException {
		Assert.isNotNull(client);
		Assert.isNotNull(method);

		final IOperationMonitor monitor = OperationUtil.convert(progress);
		ICancellableOperation operation = new ICancellableOperation() {
			@Override
			public void abort() {
				method.abort();
			}

			@Override
			public boolean isCanceled() {
				return monitor.isCanceled();
			}
		};

		CancellableOperationMonitorThread thread = null;
		if (context != null) {
			thread = (CancellableOperationMonitorThread) context.getAttribute(CONTEXT_KEY_MONITOR_THREAD);
		}
		if (thread != null) {
			thread.addOperation(operation);
		}
		try {
			return client.execute(host, method, context);
		} catch (InterruptedIOException e) {
			if (monitor.isCanceled()) {
				throw new OperationCanceledException();
			}
			throw e;
		} finally {
			if (thread != null) {
				thread.removeOperation(operation);
			}
		}
	}

	public static NTCredentials getNtCredentials(UserCredentials credentials, String workstation) {
		String username = credentials.getUserName();
		int i = username.indexOf("\\"); //$NON-NLS-1$
		if (i > 0 && i < username.length() - 1) {
//			try {
//				InetAddress localHost = InetAddress.getLocalHost();
//				if (localHost != null) {
//					hostName = localHost.getHostName();
//				}
//			} catch (UnknownHostException e) {
//				StatusHandler.log(new Status(IStatus.ERROR, ID_PLUGIN,
//						"Unable to get hostname.  Defaulting to servers host.", e));
//			}
			return new NTCredentials(username.substring(i + 1), credentials.getPassword(), workstation,
					username.substring(0, i));
		}
		return null;
	}

	public static UsernamePasswordCredentials getUserNamePasswordCredentials(UserCredentials credentials) {
		return new UsernamePasswordCredentials(credentials.getUserName(), credentials.getPassword());
	}

	public static InputStream getResponseBodyAsStream(HttpEntity entity, IProgressMonitor monitor) throws IOException {
		monitor = OperationUtil.convert(monitor);
		return entity.getContent();
//		return new PollingInputStream(new TimeoutInputStream(entity.getContent(), BUFFER_SIZE, POLL_INTERVAL,
//				CLOSE_TIMEOUT), POLL_ATTEMPTS, monitor);
	}

	public static SchemeRegistry getSchemeRegistry() {
		SchemeRegistry schemeRegistry = new SchemeRegistry();
		schemeRegistry.register(new Scheme("http", HTTP_PORT, socketFactory)); //$NON-NLS-1$
		schemeRegistry.register(new Scheme("https", HTTPS_PORT, sslSocketFactory)); //$NON-NLS-1$
		return schemeRegistry;
	}

	public static void configureProxy(AbstractHttpClient client, RepositoryLocation location) {
		Assert.isNotNull(client);
		Assert.isNotNull(location);
		String url = location.getUrl();
		Assert.isNotNull(url, "The location url must not be null"); //$NON-NLS-1$

		String host = NetUtil.getHost(url);
		Proxy proxy;
		if (NetUtil.isUrlHttps(url)) {
			proxy = location.getProxyForHost(host, IProxyData.HTTPS_PROXY_TYPE);
		} else {
			proxy = location.getProxyForHost(host, IProxyData.HTTP_PROXY_TYPE);
		}

		if (proxy != null && !Proxy.NO_PROXY.equals(proxy)) {
			InetSocketAddress address = (InetSocketAddress) proxy.address();

			client.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY,
					new HttpHost(address.getHostName(), address.getPort()));

			if (proxy instanceof AuthenticatedProxy) {
				AuthenticatedProxy authProxy = (AuthenticatedProxy) proxy;
				Credentials credentials = getCredentials(authProxy.getUserName(), authProxy.getPassword(),
						address.getAddress(), false);
				if (credentials instanceof NTCredentials) {
					AuthScope proxyAuthScopeNTLM = new AuthScope(address.getHostName(), address.getPort(),
							AuthScope.ANY_REALM, AuthPolicy.NTLM);
					client.getCredentialsProvider().setCredentials(proxyAuthScopeNTLM, credentials);

					AuthScope proxyAuthScopeAny = new AuthScope(address.getHostName(), address.getPort(),
							AuthScope.ANY_REALM);
					Credentials usernamePasswordCredentials = getCredentials(authProxy.getUserName(),
							authProxy.getPassword(), address.getAddress(), true);
					client.getCredentialsProvider().setCredentials(proxyAuthScopeAny, usernamePasswordCredentials);

				} else {
					AuthScope proxyAuthScope = new AuthScope(address.getHostName(), address.getPort(),
							AuthScope.ANY_REALM);
					client.getCredentialsProvider().setCredentials(proxyAuthScope, credentials);
				}
			}
		} else {
			client.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, null);
		}
	}

	static Credentials getCredentials(final String username, final String password, final InetAddress address,
			boolean forceUserNamePassword) {
		int i = username.indexOf("\\"); //$NON-NLS-1$
		if (i > 0 && i < username.length() - 1 && address != null && !forceUserNamePassword) {
			String hostName = address.getHostName();
			try {
				InetAddress localHost = InetAddress.getLocalHost();
				if (localHost != null) {
					hostName = localHost.getHostName();
				}
			} catch (UnknownHostException e) {
				StatusHandler.log(new Status(IStatus.ERROR, ID_PLUGIN,
						"Unable to get hostname.  Defaulting to servers host.", e)); //$NON-NLS-1$
			}
			if (hostName == null) {
				hostName = address.getHostName();
			}
			return new NTCredentials(username.substring(i + 1), password, hostName, username.substring(0, i));
		} else {
			return new UsernamePasswordCredentials(username, password);
		}
	}

	public static synchronized ThreadSafeClientConnManager getConnectionManager() {
		if (connectionManager == null) {
			connectionManager = new ThreadSafeClientConnManager(HttpUtil.getSchemeRegistry());
			if (CoreUtil.TEST_MODE) {
				connectionManager.setDefaultMaxPerRoute(2);
			} else {
				connectionManager.setDefaultMaxPerRoute(100);
				connectionManager.setMaxTotal(1000);
			}

			IdleConnectionMonitorThread thread = new IdleConnectionMonitorThread(CONNECTION_TIMEOUT_INTERVAL);
			thread.setTimeout(CONNNECT_TIMEOUT);
			thread.addConnectionManager(connectionManager);
			thread.start();
		}
		return connectionManager;
	}

	public static String getStatusText(int statusCode) {
		return EnglishReasonPhraseCatalog.INSTANCE.getReason(statusCode, Locale.getDefault());
	}

	public static void release(HttpRequest request, HttpResponse response, IProgressMonitor monitor) {
		Assert.isNotNull(request);
		Assert.isNotNull(response);
		if (monitor != null && monitor.isCanceled() && request instanceof HttpUriRequest) {
			// force a connection close on cancel to avoid blocking to do reading the remainder of the response
			try {
				((HttpUriRequest) request).abort();
			} catch (UnsupportedOperationException e) {
				// fall back to standard close
				consume(request, response);
			}
		} else {
			consume(request, response);
		}
	}

	private static void consume(HttpRequest request, HttpResponse response) {
		try {
			EntityUtils.consume(response.getEntity());
		} catch (IOException e) {
			// if construction of the stream fails the connection has to be aborted to be released
			try {
				((HttpUriRequest) request).abort();
			} catch (UnsupportedOperationException e2) {
			}
		} catch (NullPointerException e2) {
			// XXX work-around for bug 368830
		}
	}

}
