package simplecloud.storage.providers.amazon;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import simplecloud.storage.StorageAdapter;
import base.interfaces.IItem;
import base.interfaces.IResponse;

/*
 * Adapter for interacting with Amazon S3.
 * 
 * TODO: support different geographic locations
 */
public class S3Adapter extends StorageAdapter {
	
	private final String HOST;
	
	/*
	 * Constant keys for options.
	 */
	public enum Type {
		SRC_BUCKET,
		DEST_BUCKET;
	}
	
	/*
	 * Constructs the adapter.
	 * 
	 * @param accessId is the Amazon account access ID
	 * @param accessKey is the corresponding key to the Amazon account
	 * @param host identifies the endpoint for the S3 services
	 */
	public S3Adapter(String accessId, String accessKey, String host) {
		HOST = host;
		connection = new S3Connection(accessId, accessKey, host, this);
	}

	/*
	 * Required options: Type.SRC_BUCKET
	 */
	public IItem fetchItem(String path, Map<Object, Object> options) {
		String bucket = (String)options.get(Type.SRC_BUCKET);
		connection.connect(bucket + '.' + HOST);
		IResponse response = connection.request("GET", path, null, null, null);
		S3Handler rh = new S3Handler(response);
		return (IItem)rh.getParsedObject();
	}

	/* 
	 * Required options: Type.SRC_BUCKET
	 */
	public boolean storeItem(String destinationPath, IItem item, Map<String, String> metadata, Map<Object, Object> options) {
		String bucket = (String)options.get(Type.SRC_BUCKET);
		connection.connect(bucket + '.' + HOST);
		IResponse response = connection.request("PUT", destinationPath, metadata, null, item);
		new S3Handler(response);
		return response.getStatus() == 200;
	}
	
	/*
	 * Required options: Type.SRC_BUCKET
	 */
	public void deleteItem(String path, Map<Object, Object> options) {
		String bucket = (String)options.get(Type.SRC_BUCKET);
		connection.connect(bucket + '.' + HOST);
		IResponse response = connection.request("DELETE", path, null, null, null);
		new S3Handler(response);
	}

	/*
	 * Required options: Type.SRC_BUCKET, Type.DEST_BUCKET
	 */
	public void copyItem(String sourcePath, String destinationPath, Map<Object, Object> options) {
		String srcBucket = (String)options.get(Type.SRC_BUCKET);
		String destBucket = (String)options.get(Type.DEST_BUCKET);
		connection.connect(destBucket + '.' + HOST);
		Map<String, String> headers = new HashMap<String, String>();
		headers.put("x-amz-copy-source", '/' + srcBucket + sourcePath);
		IResponse response = connection.request("PUT", destinationPath, headers, null, null);
		new S3Handler(response);
	}

	/*
	 * Required options: Type.SRC_BUCKET, Type.DEST_BUCKET
	 */
	public void moveItem(String sourcePath, String destinationPath, Map<Object, Object> options) {
		// Copy the item
		String srcBucket = (String)options.get(Type.SRC_BUCKET);
		String destBucket = (String)options.get(Type.DEST_BUCKET);
		connection.connect(destBucket + '.' + HOST);
		Map<String, String> headers = new HashMap<String, String>();
		headers.put("x-amz-copy-source", '/' + srcBucket + sourcePath);
		IResponse response = connection.request("PUT", destinationPath, headers, null, null);
		new S3Handler(response);
		
		// Delete the original
		connection.connect(srcBucket + '.' + HOST);
		response = connection.request("DELETE", sourcePath, null, null, null);
		new S3Handler(response);
	}

	/*
	 * Required options: Type.SRC_BUCKET
	 */
	public void renameItem(String path, String name, Map<Object, Object> options) {
		String destPath = path.substring(0, path.lastIndexOf('/') + 1) + name;
		options.put(Type.DEST_BUCKET, options.get(Type.SRC_BUCKET));
		moveItem(path, destPath, options);
	}

	/*
	 * Required options: Type.SRC_BUCKET
	 */
	public List<String> listItems(String path, Map<Object, Object> options) {
		String bucket = (String)options.get(Type.SRC_BUCKET);
		connection.connect(bucket + '.' + HOST);
		IResponse response = connection.request("GET", path, null, null, null);
		S3Handler rh = new S3Handler(response);
		IItem item = (IItem)rh.getParsedObject();
		try {
			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
			DocumentBuilder db = dbf.newDocumentBuilder();
			Document doc = db.parse(item.getContent());
			return parseItems(doc.getDocumentElement());
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return null;	// Error
	}

	/*
	 * Required options: Type.SRC_BUCKET
	 */
	public Map<String, String> fetchMetadata(String path, Map<Object, Object> options) {
		String bucket = (String)options.get(Type.SRC_BUCKET);
		connection.connect(bucket + '.' + HOST);
		IResponse response = connection.request("GET", path, null, null, null);
		new S3Handler(response);
		return response.getHeaders();
	}

	/*
	 * The key/value metadata is forwarded directly to the server without
	 * interpretation.
	 * 
	 * Required options: Type.SRC_BUCKET
	 */
	public void storeMetadata(String destinationPath, Map<String, String> metadata, Map<Object, Object> options) {
		String bucket = (String)options.get(Type.SRC_BUCKET);
		connection.connect(bucket + '.' + HOST);
		IResponse response = connection.request("PUT", destinationPath, metadata, null, null);
		new S3Handler(response);
	}

	/*
	 * Amazon S3 does not currently support deleting metadata.
	 * 
	 * Workaround: fetchItem, deleteItem, then storeItem with updated metadata
	 */
	public void deleteMetadata(String path, Map<Object, Object> options) {
		throw new RuntimeException("Deleting metadata is not supported");
	}
	
	private List<String> parseItems(Element elem) {
		XPath xpath = XPathFactory.newInstance().newXPath();
		try {
			List<String> items = new ArrayList<String>();
			NodeList keys = (NodeList)xpath.evaluate("Contents/Key", elem, XPathConstants.NODESET);
			for (int i = 0; i < keys.getLength(); i++) {
				items.add(keys.item(i).getTextContent());
			}
			return items;
		} catch (XPathExpressionException e) {
			e.printStackTrace();
		}
		return null;	// Error
	}
}
