package simplecloud.storage.providers.nirvanix;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.InputStreamBody;
import org.apache.http.impl.client.DefaultHttpClient;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import simplecloud.storage.interfaces.IStorageAdapter;
import base.Driver;
import base.connection.ConnectionKey;
import base.connection.Response;
import base.interfaces.IItem;
import base.interfaces.IResponse;

/*
 * TODO: comment required option parameters
 * TODO: general comments
 * TODO: test url encoding of paths
 */
public class NirvanixAdapter extends Driver implements IStorageAdapter {
	
	private static final String HOST = "services.nirvanix.com";
	
	private String sessionToken;
	
	private String appName;
	
	private String account;
	
	/*
	 * Constant keys for option parameters.
	 */
	public enum Type {
		PAGE_NUMBER,
		PAGE_SIZE,
		ITEM_TYPE,
		FILE,
		FOLDER;
	}
	
	public NirvanixAdapter(String appName, String appKey, String username, String password) {
		this.connection = new NirvanixConnection(appKey, username, password, HOST, this);
		this.sessionToken = ((NirvanixConnection)connection).getSessionToken();
		this.appName = appName;
		this.account = username;
	}

	public IItem fetchItem(String path, Map<Object, Object> options) {
		// First get the DownloadNode
		Map<String, String> nodeParams = new HashMap<String, String>();
		nodeParams.put("sessionToken", sessionToken);
		nodeParams.put("filePath", path);
		IResponse nodeResponse = connection.request("GET", "/ws/IMFS/GetDownloadNodes.ashx", null, nodeParams, null);
		NirvanixHandler handler = new NirvanixHandler(nodeResponse);
		String node = parseDownloadNodes((Document)handler.getParsedObject()).get(0);
		
		// Download content from node
		Map<String, String> downloadParams = new HashMap<String, String>();
		downloadParams.put("sessionToken", sessionToken);
		ConnectionKey downloadConnection = new ConnectionKey(null, true, node, this);
		String action;
		try {
			action = '/' + appName + '/' + account + URLEncoder.encode(path, "UTF-8");
			IResponse downloadResponse = downloadConnection.request("GET", action, null, downloadParams, null);
			if (downloadResponse.getStatus() == 200) return downloadResponse.getItem();
			new NirvanixHandler(downloadResponse);
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		return null;	// Error
	}

	/*
	 * TODO: This was hacked together because of lacking multipart-form posts in ConnectionKey.
	 * TODO: metadata
	 */
	public boolean storeItem(String destinationPath, IItem item, Map<String, String> metadata, Map<Object, Object> options) {
		// First get the StorageNode
		Map<String, String> nodeParams = new HashMap<String, String>();
		nodeParams.put("sessionToken", sessionToken);
		nodeParams.put("sizeBytes", String.valueOf(item.getContentLength()));
		IResponse nodeResponse = connection.request("GET", "/ws/IMFS/GetStorageNode.ashx", null, nodeParams, null);
		NirvanixHandler handler = new NirvanixHandler(nodeResponse);
		String uploadHost = parseUploadHost((Document)handler.getParsedObject());
		String uploadToken = parseUploadToken((Document)handler.getParsedObject());
		
		// Parse directory & filename
		String directory = destinationPath.substring(0, destinationPath.lastIndexOf('/') + 1);
		String filename = destinationPath.substring(destinationPath.lastIndexOf('/'), destinationPath.length());
		
		// Create URI endpoint
		Map<String, String> uploadParams = new HashMap<String, String>();
		uploadParams.put("uploadToken", uploadToken);
		uploadParams.put("destFolderPath", directory);
		URI uri = null;
		try {
			uri = URIUtils.createURI("http", uploadHost, -1, "/Upload.ashx", ConnectionKey.urlEncodeMap(uploadParams), null);
		} catch (URISyntaxException e) {
			e.printStackTrace();
			return false;
		}
		HttpPost httppost = new HttpPost(uri);
		
		// Build request entity
		MultipartEntity entity = new MultipartEntity();
		InputStreamBodyLength isbl = new InputStreamBodyLength(item.getContent(), item.getContentType(), filename, item.getContentLength());
		entity.addPart("fileContent", isbl);
		httppost.setEntity(entity);
		
		// POST file to the server
		HttpClient httpclient = new DefaultHttpClient();
		try {
			IResponse response = new Response(httpclient.execute(httppost));
			new NirvanixHandler(response);
			return true;
		} catch (ClientProtocolException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return false;	// Error
	}

	public void deleteItem(String path, Map<Object, Object> options) {
		Map<String, String> params = new HashMap<String, String>();
		params.put("sessionToken", sessionToken);
		Type itemType = (Type)options.get(Type.ITEM_TYPE);
		IResponse response = null;
		if (itemType == Type.FOLDER) {
			params.put("folderPath", path);
			response = connection.request("GET", "/ws/IMFS/DeleteFolders.ashx", null, params, null);
		} else {
			params.put("filePath", path);
			response = connection.request("GET", "/ws/IMFS/DeleteFiles.ashx", null, params, null);
		}
		new NirvanixHandler(response);
	}

	public void copyItem(String sourcePath, String destinationPath, Map<Object, Object> options) {
		Map<String, String> params = new HashMap<String, String>();
		params.put("sessionToken", sessionToken);
		Type itemType = (Type)options.get(Type.ITEM_TYPE);
		IResponse response = null;
		if (itemType == Type.FOLDER) {
			params.put("srcFolderPath", sourcePath);
			params.put("destFolderPath", destinationPath);
			response = connection.request("GET", "/ws/IMFS/CopyFolders.ashx", null, params, null);
		} else {
			params.put("srcFilePath", sourcePath);
			params.put("destFolderPath", destinationPath);
			response = connection.request("GET", "/ws/IMFS/CopyFiles.ashx", null, params, null);
		}
		new NirvanixHandler(response);
	}

	public void moveItem(String sourcePath, String destinationPath, Map<Object, Object> options) {
		Map<String, String> params = new HashMap<String, String>();
		params.put("sessionToken", sessionToken);
		Type itemType = (Type)options.get(Type.ITEM_TYPE);
		IResponse response = null;
		if (itemType == Type.FOLDER) {
			params.put("srcFolderPath", sourcePath);
			params.put("destFolderPath", destinationPath);
			response = connection.request("GET", "/ws/IMFS/MoveFolders.ashx", null, params, null);
		} else {
			params.put("srcFilePath", sourcePath);
			params.put("destFolderPath", destinationPath);
			response = connection.request("GET", "/ws/IMFS/MoveFiles.ashx", null, params, null);
		}
		new NirvanixHandler(response);
	}

	public void renameItem(String path, String name, Map<Object, Object> options) {
		Map<String, String> params = new HashMap<String, String>();
		params.put("sessionToken", sessionToken);
		Type itemType = (Type)options.get(Type.ITEM_TYPE);
		IResponse response = null;
		if (itemType == Type.FOLDER) {
			params.put("folderPath", path);
			params.put("newFolderName", name);
			response = connection.request("GET", "/ws/IMFS/RenameFolder.ashx", null, params, null);
		} else {
			params.put("filePath", path);
			params.put("newFileName", name);
			response = connection.request("GET", "/ws/IMFS/RenameFile.ashx", null, params, null);
		}
		new NirvanixHandler(response);
	}

	public List<String> listItems(String path, Map<Object, Object> options) {
		Map<String, String> params = new HashMap<String, String>();
		params.put("sessionToken", sessionToken);
		params.put("folderPath", path);
		params.put("pageNumber", (String)options.get(Type.PAGE_NUMBER));
		params.put("pageSize", (String)options.get(Type.PAGE_SIZE));
		IResponse response = connection.request("GET", "/ws/IMFS/ListFolder.ashx" + path, null, params, null);
		NirvanixHandler rh = new NirvanixHandler(response);
		return parseItems((Document)rh.getParsedObject());
	}

	public Map<String, String> fetchMetadata(String path, Map<Object, Object> options) {
		Map<String, String> params = new HashMap<String, String>();
		params.put("sessionToken", sessionToken);
		params.put("path", path);
		IResponse response = connection.request("GET", "/ws/Metadata/GetMetadata.ashx", null, params, null);
		NirvanixHandler handler = new NirvanixHandler(response);
		return parseMetadata((Document)handler.getParsedObject());
	}

	/*
	 * NOTE: Because Nirvanix specifies multiple metadata key/value pairs with
	 * the same key, and a Map is used to store the key/value pairs, only one
	 * metadata item may be stored at a time.
	 * 
	 * TODO: Possibly use List<KeyValuePair> for headers/params? Map does not
	 * support multi-mapping.
	 */
	public void storeMetadata(String destinationPath, Map<String, String> metadata, Map<Object, Object> options) {
		if (metadata.keySet().size() != 1) {
			throw new RuntimeException("Support limitation: exactly one metadata key/value pair must be set");
		}
		
		String metadataParam = "";
		for (String key : metadata.keySet()) {
			metadataParam += key + ":" + metadata.get(key);
		}
		
		Map<String, String> params = new HashMap<String, String>();
		params.put("sessionToken", sessionToken);
		params.put("path", destinationPath);
		params.put("metadata", metadataParam);
		IResponse response = connection.request("GET", "/ws/Metadata/SetMetadata.ashx", null, params, null);
		new NirvanixHandler(response);
	}

	public void deleteMetadata(String path, Map<Object, Object> options) {
		Map<String, String> params = new HashMap<String, String>();
		params.put("sessionToken", sessionToken);
		params.put("path", path);
		IResponse response = connection.request("GET", "/ws/Metadata/DeleteAllMetadata.ashx", null, params, null);
		new NirvanixHandler(response);
	}
	
	private List<String> parseDownloadNodes(Document doc) {
		try {
			Element elem = doc.getDocumentElement();
			XPath xpath = XPathFactory.newInstance().newXPath();
			List<String> nodes = new ArrayList<String>();
			NodeList downloadNodes = (NodeList)xpath.evaluate("/Response/DownloadNode", elem, XPathConstants.NODESET);
			for (int i = 0; i < downloadNodes.getLength(); i++) {
				nodes.add(downloadNodes.item(i).getTextContent());
			}
			return nodes;
		} catch (XPathExpressionException e) {
			e.printStackTrace();
		}
		return null;	// Error
	}
	
	private String parseUploadHost(Document doc) {
		try {
			XPath xpath = XPathFactory.newInstance().newXPath();
			return xpath.evaluate("/Response/GetStorageNode/UploadHost", doc.getDocumentElement());
		} catch (XPathExpressionException e) {
			e.printStackTrace();
		}
		return null;	// Error
	}
	
	private String parseUploadToken(Document doc) {
		try {
			XPath xpath = XPathFactory.newInstance().newXPath();
			return xpath.evaluate("/Response/GetStorageNode/UploadToken", doc.getDocumentElement());
		} catch (XPathExpressionException e) {
			e.printStackTrace();
		}
		return null;	// Error
	}

	private List<String> parseItems(Document doc) {
		try {
			Element elem = doc.getDocumentElement();
			XPath xpath = XPathFactory.newInstance().newXPath();
			List<String> items = new ArrayList<String>();
			NodeList folders = (NodeList)xpath.evaluate("/Response/ListFolder/Folder/Name", elem, XPathConstants.NODESET);
			for (int i = 0; i < folders.getLength(); i++) {
				items.add(folders.item(i).getTextContent());
			}
			NodeList files = (NodeList)xpath.evaluate("/Response/ListFolder/File/Name", elem, XPathConstants.NODESET);
			for (int i = 0; i < files.getLength(); i++) {
				items.add(files.item(i).getTextContent());
			}
			return items;
		} catch (XPathExpressionException e) {
			e.printStackTrace();
		}
		return null;	// Error
	}
	
	private Map<String, String> parseMetadata(Document doc) {
		try {
			Map<String, String> metadata = new HashMap<String, String>();
			XPath xpath = XPathFactory.newInstance().newXPath();
			NodeList types = (NodeList)xpath.evaluate("/Response/Metadata/Type", doc.getDocumentElement(), XPathConstants.NODESET);
			NodeList values = (NodeList)xpath.evaluate("/Response/Metadata/Value", doc.getDocumentElement(), XPathConstants.NODESET);
			for (int i = 0; i < types.getLength(); i++) {
				metadata.put(types.item(i).getTextContent(), values.item(i).getTextContent());
			}
			return metadata;
		} catch (XPathExpressionException e) {
			e.printStackTrace();
			return null;
		}
	}
	
	/*
	 * Simple extension of InputStreamBody to support length.
	 * 
	 * TODO: this is simply a hack until HttpClient-alpha2 matures
	 */
	private class InputStreamBodyLength extends InputStreamBody {
		private long length;
		public InputStreamBodyLength(InputStream in, String mimeType,
				String filename, long length) {
			super(in, mimeType, filename);
			this.length = length;
		}
		public long getContentLength() {
			return length;
		}
	}
}
