package base.connection;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;

import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;

import base.Driver;
import base.interfaces.IConnection;
import base.interfaces.IItem;

/**
 * Manages a connection with a secret access key.
 */
public class ConnectionKey implements IConnection {

	/** The host server. */
	protected String host;

	/** The server's port. */
	protected int port;

	/** Identifies the user's account. */
	protected String accessId;

	/** The account user's secret key/password. */
	protected String accessKey;

	/** Indicates if the connection is secure.  Defaults to true. */
	protected boolean secure;

	/** References the driver that owns this connection. */
	protected Driver driver;
	
	/** HTTP operation (i.e. GET, DELETE, PUT, POST). */
	protected String method;
	
	/** Path to the resource being accessed (e.g. /john.doe/images/1) */
	protected String path;

	/** Client used to make requests to the server. */
	public DefaultHttpClient httpClient;
	
	/**
	 * Constructs this connection with the default port.
	 * 
	 * @param accessKey is the user's secret key used for authentication
	 * @param secure indicates if this connection is secure or not
	 * @param host is the host to connect to
	 * @param driver references the driver that owns this connection
	 */
	public ConnectionKey(String accessKey, boolean secure, String host, Driver driver) {
		this.accessKey = accessKey;
		this.secure = secure;
		this.host = host;
		this.driver = driver;
		this.accessId = null;	// subclass may initialize
		this.port = -1;			// default port used by HTTPComponents
		this.path = null;
	}

	/**
	 * Constructs this connection.
	 * 
	 * @param accessKey is the user's secret key used for authentication
	 * @param secure indicates if this connection is secure
	 * @param host is the host to connect to
	 * @param port is the host's port to connect to
	 * @param driver references the driver that owns this connection
	 */
	public ConnectionKey(String accessKey, boolean secure, String host, int port,
			Driver driver) {
		this.accessKey = accessKey;
		this.secure = secure;
		this.host = host;
		this.port = port;
		this.driver = driver;
		this.httpClient = null;
		this.accessId = null;	// subclass may initialize
		this.path = null;
	}

	public void connect() {
		connect(host, port);
	}
	
	public void connect(String host) {
		connect(host, -1);
	}

	public void connect(String host, int port) {
		this.host = host;
		this.port = port;

		httpClient = new DefaultHttpClient();
		if (accessId != null) {
			httpClient.getCredentialsProvider().setCredentials(
					new AuthScope(host, port),
					new UsernamePasswordCredentials(accessId, accessKey));
		}
	}

	/*
	 * TODO: I think some drivers could be simplified if request() was
	 * overloaded to take in a full URL, including host & action.  This is
	 * especially true for drivers that require multiple requests to retrieve
	 * e.g. a session token or DownloadNode for downloading content.  This
	 * would prevent need to re-connect() for each of these requests.
	 * 
	 * TODO: Rewrite in support of:
	 * 	string body content
	 * 	inputstream body content (use Item?)
	 * 	multipart form posts
	 * 	change to encodeData(Object data) for generic processing
	 * 
	 * TODO: use List<KeyValuePair> for headers/params? Map does not support one-many mapping
	 */
	public Response request(String method, String path,
			Map<String, String> headers, Map<String, String> params, Object data) {
		this.method = method;
		this.path = path;
		IItem item = data instanceof IItem ? (IItem)data : null;
		
		// Connect if not already connected
		if (httpClient == null) {
			connect();
		}
		
		// Compose request headers with priority
		Map<String, String> reqHeaders = new HashMap<String, String>();
		reqHeaders.put("User-Agent", getUserAgent());		// 3: libcloud headers
		reqHeaders.put("Host", host);
		if (item != null) {									// 4: item headers
			if (item.getContentType() != null) reqHeaders.put("Content-Type", item.getContentType());
			if (item.getContentEncoding() != null) reqHeaders.put("Content-Encoding", item.getContentEncoding());
		}
		if (headers != null) reqHeaders.putAll(headers);	// 2: argument headers
		reqHeaders = addDefaultHeaders(reqHeaders);			// 1: provider headers
		
		// Compose request parameters with priority
		Map<String, String> reqParams = (params != null ? params	// 2: argument parameters
				: new HashMap<String, String>());
		reqParams = addDefaultParams(reqParams);					// 1: provider parameters

		// Create URI endpoint
		URI uri = null;
		try {
			String protocol = secure ? "https" : "http";
			uri = URIUtils.createURI(protocol, host, port, path, urlEncodeMap(reqParams), null);
		} catch (URISyntaxException e) {
			e.printStackTrace();
			return null;
		}
		
		// Create request to send
		HttpUriRequest request = null;
		if (method.equalsIgnoreCase("GET")) {
			request = new HttpGet(uri);
		} else if (method.equalsIgnoreCase("DELETE")) {
			request = new HttpDelete(uri);
		} else if (method.equalsIgnoreCase("PUT")) {
			request = new HttpPut(uri);
		} else if (method.equalsIgnoreCase("POST")) {
			request = new HttpPost(uri);
		} else {
			throw new IllegalArgumentException("Illegal HTTP method: " + method);
		}
		
		// Encode and add the payload data
		if (data instanceof IItem) {
			InputStreamEntity entity = new InputStreamEntity(item.getContent(), item.getContentLength());
			entity.setContentType(item.getContentType());
			entity.setContentEncoding(item.getContentEncoding());
			((HttpEntityEnclosingRequest)request).setEntity(entity);
		} else {
			String encoded = encodeData((String)data);
			if (encoded != null && !encoded.equals("")) {
				try {
					((HttpEntityEnclosingRequest) request).setEntity(new StringEntity(encoded));
				} catch (UnsupportedEncodingException e) {
					e.printStackTrace();
					return null;
				}
			}
		}

		// Add headers to the request
		for (String header : reqHeaders.keySet()) {
			request.addHeader(header, reqHeaders.get(header));
		}

		// Execute the request!
		try {
			return new Response(httpClient.execute(request));
		} catch (ClientProtocolException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

		return null; // Error
	}
	
	/**
	 * Utility that performs UTF-8 encoding of a map of Strings. Returns a
	 * String that is suitable for use as an application/x-www-form-urlencoded
	 * list of parameters in an HTTP PUT or HTTP POST.
	 * 
	 * @param map is the map of String values to format as a UTF-8 String
	 * 
	 * @return String is the UTF-8 encoding of the map of String values
	 */
	public static String urlEncodeMap(Map<String, String> map) {
		if (map == null) return null;
		
		// Convert map to List<NameValuePair> for URLEncodedUtils
		List<BasicNameValuePair> nvp = new ArrayList<BasicNameValuePair>();
		for (String key : new TreeSet<String>(map.keySet())) {	// preserve order
			nvp.add(new BasicNameValuePair(key, map.get(key)));
		}
		return URLEncodedUtils.format(nvp, "UTF-8");
	}
	
	// ============================ PROTECTED METHODS ===========================
	protected Map<String, String> addDefaultHeaders(Map<String, String> headers) {
		return headers;
	}

	protected Map<String, String> addDefaultParams(Map<String, String> params) {
		return params;
	}

	protected String encodeData(String data) {
		return data;
	}
	
	// ============================ PRIVATE METHODS ===========================
	/*
	 * Defines the user agent used when making requests.
	 * 
	 * @return String is the user agent for making requests
	 */
	private String getUserAgent() {
		return "libcloud-java";
	}	
}
