/*
 * Copyright (C) 2011 lightcouch.org
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.lightcouch;

import java.io.IOException;
import java.net.Socket;
import java.net.URLDecoder;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.RequestLine;
import org.apache.http.auth.AuthScheme;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.AuthState;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.AbstractHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;

/**
 * Presents a <i>client</i> to CouchDB database server; targeted to run on Android platform.
 * @see CouchDbClient
 * @since 0.1.0
 * @author Ahmed Yehia
 */
@SuppressWarnings("deprecation")
public class CouchDbClientAndroid extends CouchDbClientBase {
	
	/**
	 * Constructs a new client instance.
	 */
	public CouchDbClientAndroid() {
		super();
	}
	
	/**
	 * Constructs a new client instance.
	 * @param configFileName The configuration file
	 */
	public CouchDbClientAndroid(String configFileName) {
		super(new CouchDbConfig(configFileName));
	}
	
	/**
	 * Constructs a new client instance.
	 * @param dbName The database name.
	 * @param createDbIfNotExist To create a new database if it does not already exist.
	 * @param protocol The protocol to use (i.e http or https)
	 * @param host The database host address
	 * @param port The database listening port
	 * @param username The Username credential
	 * @param password The Password credential
	 */
	public CouchDbClientAndroid(String dbName, boolean createDbIfNotExist, 
			String protocol, String host, int port, String username, String password) { 		
		super(new CouchDbConfig(new CouchDbProperties(dbName, createDbIfNotExist, protocol, host, port, username, password)));
	}
	
	/**
	 * Constructs a new client instance.
	 * @param properties An object containing configuration properties.
	 */
	public CouchDbClientAndroid(CouchDbProperties properties) {
		super(new CouchDbConfig(properties));
	}
	
	/**
	 * @return {@link DefaultHttpClient} instance.
	 */
	@Override
	HttpClient createHttpClient(CouchDbProperties props) {
		DefaultHttpClient httpclient = null;
		try {
			final SchemeRegistry schemeRegistry = createRegistry(props);
			// Http params
			final HttpParams params = new BasicHttpParams();
			params.setParameter(CoreProtocolPNames.HTTP_CONTENT_CHARSET, "UTF-8");
			params.setParameter(CoreConnectionPNames.SO_TIMEOUT, props.getSocketTimeout());
			params.setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, props.getConnectionTimeout());
			final ThreadSafeClientConnManager ccm = new ThreadSafeClientConnManager(params,schemeRegistry);
			httpclient = new DefaultHttpClient(ccm, params);
			if(props.getProxyHost() != null) {
				HttpHost proxy = new HttpHost(props.getProxyHost(), props.getProxyPort());
				httpclient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
			}
			// basic authentication
			if(props.getUsername() != null && props.getPassword() != null) {
				httpclient.getCredentialsProvider().setCredentials(
						new AuthScope(props.getHost(), props.getPort()),
						new UsernamePasswordCredentials(props.getUsername(), props.getPassword()));
				props.clearPassword();
			}
			registerInterceptors(httpclient);
		} catch (Exception e) {
			throw new IllegalStateException("Error Creating HTTP client. ", e);
		}
		return httpclient;
	}
	
	@Override
	HttpContext createContext() {	
		HttpContext context = new BasicHttpContext();
		BasicScheme basicAuth = new BasicScheme();
		context.setAttribute("preemptive-auth", basicAuth);
		((AbstractHttpClient) httpClient).addRequestInterceptor(new PreemptiveAuthInterceptor(), 0);
		return context;
	}

	@Override
	public void shutdown() {
		this.httpClient.getConnectionManager().shutdown();
	}

	private SchemeRegistry createRegistry(CouchDbProperties properties) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, CertificateException, IOException, UnrecoverableKeyException {
		SchemeRegistry registry = new SchemeRegistry();
		if("https".equals(properties.getProtocol())) {
			KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
	        trustStore.load(null, null);
	        SSLSocketFactory sf = new MySSLSocketFactory(trustStore);
	        sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
			registry.register(new Scheme(properties.getProtocol(), sf, properties.getPort()));
		} else {
			registry.register(new Scheme(properties.getProtocol(), PlainSocketFactory.getSocketFactory(), properties.getPort()));
		}
		return registry;
	}
	
	private void registerInterceptors(DefaultHttpClient httpclient) {
		httpclient.addRequestInterceptor(new HttpRequestInterceptor() {
		    public void process(
		            final HttpRequest request,
		            final HttpContext context) throws IOException {
		        if (log.isInfoEnabled()) {
					RequestLine req = request.getRequestLine();
					log.info("> " + req.getMethod() + URLDecoder.decode(req.getUri(), "UTF-8"));
		        }
		    }
		});
		httpclient.addResponseInterceptor(new HttpResponseInterceptor() {
		    public void process(
		            final HttpResponse response,
		            final HttpContext context) throws IOException {
		    	if(log.isInfoEnabled()) {
					log.info("< Status: " + response.getStatusLine().getStatusCode());
		    	}
		    	validate(response);
		    }
		});
	}
	
	private static class MySSLSocketFactory extends SSLSocketFactory {
		SSLContext sslContext = SSLContext.getInstance("TLS");

		public MySSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
			super(truststore);

			TrustManager tm = new X509TrustManager() {
				public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
				}

				public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
				}

				public X509Certificate[] getAcceptedIssuers() {
					return null;
				}
			};

			sslContext.init(null, new TrustManager[] { tm }, null);
		}

		@Override
		public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException, UnknownHostException {
			return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose);
		}

		@Override
		public Socket createSocket() throws IOException {
			return sslContext.getSocketFactory().createSocket();
		}
	}
	
	private static class PreemptiveAuthInterceptor implements HttpRequestInterceptor {

	    public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
	        AuthState authState = (AuthState) context.getAttribute(ClientContext.TARGET_AUTH_STATE);

	        // If no auth scheme avaialble yet, try to initialize it preemptively
	        if (authState.getAuthScheme() == null) {
	            AuthScheme authScheme = (AuthScheme) context.getAttribute("preemptive-auth");
	            CredentialsProvider credsProvider = (CredentialsProvider) context.getAttribute(ClientContext.CREDS_PROVIDER);
	            HttpHost targetHost = (HttpHost) context.getAttribute(ExecutionContext.HTTP_TARGET_HOST);
	            if (authScheme != null) {
	            	authState.setAuthScheme(authScheme);
	                Credentials creds = credsProvider.getCredentials(new AuthScope(targetHost.getHostName(), targetHost.getPort()));
	                if (creds != null) {
	                	authState.setCredentials(creds);
	                }
	            }
	        }

	    }

	}
}