package simplecloud.storage.providers.amazon;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.SimpleTimeZone;
import java.util.TreeMap;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;

import base.Driver;
import base.connection.ConnectionUserAndKey;

public class S3Connection extends ConnectionUserAndKey {
	
	private static final boolean DEFAULT_SECURE = true;
	
	private static List<String> subResources;

	public S3Connection(String accessId, String accessKey, String host,
			Driver driver) {
		super(accessId, accessKey, DEFAULT_SECURE, host, driver);
		subResources = new ArrayList<String>();
		subResources.add("?versioning");
		subResources.add("?location");
		subResources.add("?acl");
		subResources.add("?torret");
		subResources.add("?logging");
	}
	
	protected Map<String, String> addDefaultHeaders(Map<String, String> headers) {
		headers.put("Host", this.host);
		SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'");
		sdf.setTimeZone(new SimpleTimeZone(0, "GMT"));
		headers.put("Date", sdf.format(new Date()));
		String signature = getSignature(headers);
		headers.put("Authorization", "AWS " + accessId + ':' + signature);
		return headers;
	}
	
	protected Map<String, String> addDefaultParams(Map<String, String> params) {
		return null;
	}
	
	/*
	 * Generates the request signature as documented at:
	 * http://docs.amazonwebservices.com/AmazonS3/2006-03-01/dev/index.html
	 * 
	 * @param headers are the headers used to generate the signature
	 * 
	 * @return the signature that signs (authenticates) the request
	 */
	private String getSignature(Map<String, String> headers) {
		String stringToSign = getStringToSign(headers);
		
		// Calculate HMAC of stringToSign
		byte[] hmac = null;
		try {
			Mac mac = Mac.getInstance("HmacSHA1");
			SecretKeySpec key = new SecretKeySpec(accessKey.getBytes(), "UTF-8");
			mac.init(key);
			hmac = mac.doFinal(stringToSign.getBytes());
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (InvalidKeyException e) {
			e.printStackTrace();
		}
		
		// Base 64 encode HMAC
		return new String(Base64.encodeBase64(hmac));
	}
	
	private String getStringToSign(Map<String, String> headers) {
		StringBuilder sb = new StringBuilder();
		sb.append(this.method).append('\n');
		if (headers.containsKey("Content-MD5")) sb.append(headers.get("Content-MD5"));
		sb.append('\n');
		if (headers.containsKey("Content-Type")) sb.append(headers.get("Content-Type"));
		sb.append('\n');
		if (containsKeyIgnoreCase(headers, "x-amz-date")) {
			sb.append('\n');
		} else {
			sb.append(headers.get("Date")).append('\n');
		}
		sb.append(getCanonicalizedAmzHeaders(headers));
		sb.append(getCanonicalizedResource(headers));
		return sb.toString();
	}
	
	private String getCanonicalizedAmzHeaders(Map<String, String> headers) {
		// 1 & 2, Compose lowercase, sorted map of x-amz headers
		TreeMap<String, String> xamzMap = new TreeMap<String, String>();
		for (String key : headers.keySet()) {
			if (key.toLowerCase().startsWith("x-amz-")) {
				xamzMap.put(key.toLowerCase(), headers.get(key));
			}
		}
		
		// 3: Combine like headers (e.g. x-amz-common: val1,val2)
		// No. Map<String, List<String>> isn't happening.
		
		// 4: Unfold long headers that span multiple lines
		// No. Don't send in long header lines.
		
		// 5 & 6: Construct final string
		StringBuilder sb = new StringBuilder();
		for (String key : xamzMap.keySet()) {
			sb.append(key).append(':').append(xamzMap.get(key)).append('\n');	// TODO: include newline on last header?
		}
		return sb.toString();
	}
	
	private String getCanonicalizedResource(Map<String, String> headers) {
		// 1: Start with empty string
		StringBuilder sb = new StringBuilder();
		
		// 2: Add bucket if present
		if (headers.containsKey("Host")) {
			String host = headers.get("Host");
			if (!host.startsWith("s3.")) {
				sb.append('/').append(host.substring(0, host.indexOf('.')));
			}
		}
		
		// 3: Add HTTP Request URI up to but not including query
		int queryIdx = path.indexOf('?');
		if (queryIdx == -1) {
			sb.append(this.path);
		} else {
			sb.append(path.substring(0, queryIdx));
			
			// 4: Add sub-resources if present
			String queryString = path.substring(queryIdx, path.length());
			for (String subResource : subResources) {
				if (queryString.startsWith(subResource)) {
					sb.append(subResource);
					break;
				}
			}
		}
		return sb.toString();
	}
	
	/*
	 * Case-agnostic function that determines if a key is contained within a
	 * map.
	 */
	private boolean containsKeyIgnoreCase(Map<String, String> map, String aKey) {
		for (String key : map.keySet())
			if (key.equalsIgnoreCase(aKey)) return true;
		return false;
	}
}
