import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.Collator;
import java.text.FieldPosition;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

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

/**
 * This class provides business delegate to access SDB web service using REST approach.
 *
 */
public class SdbDelegate {
    private static final String DEFAULT_ENCODING = "UTF-8";
    private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
    private static final String MORETOKEN_STRING = "MoreToken";
                                                                                                                                                      
    private final String accessId;
    private final String secretKey;
    private final String uri;
    private final String version;

    public static class Response {
        public final Document rootDocument;
        public final Element rootElement;
        public final String requestId;
        public final String boxUsage;
        public final String moreToken;
        public final Object result;
        public Response(Document rootDocument, Element rootElement, String requestId, String boxUsage, String moreToken, Object result) {
            this.rootDocument = rootDocument;
            this.rootElement = rootElement;
            this.requestId = requestId;
            this.boxUsage = boxUsage;
            this.moreToken = moreToken;
            this.result = result;
        }
    }

    public SdbDelegate() {
        this("key", "secretkey", "http://sdb.amazonaws.com", "2007-11-07");
    }


    public SdbDelegate(String accessId, String secretKey, String uri, String version) {
        this.accessId = accessId;
        this.secretKey = secretKey;
        this.uri = uri;
        this.version = version;
    }


    public void delete(String bucket, String name) {
        deleteAttributes(bucket, name);
    }

    public void createDomain(String domainName) {
        Map<String,String> parameters = getBaseParameters("CreateDomain");
        parameters.put("DomainName",encode(domainName));
        invoke(parameters);
    }

    public void deleteDomain(String domainName) {
        Map<String,String> parameters = getBaseParameters("DeleteDomain");
        parameters.put("DomainName",encode(domainName));
        invoke(parameters);
    }

    public List<String> listDomains() {
        return listDomains(null);
    }

    public List<String> listDomains(StringBuilder moreToken) {
        return (List<String>) listDomains(moreToken, null);
    }
                                                                                                                                                      
    public List<String> listDomains(StringBuilder moreToken, Integer maxResults) {
        Map<String,String> parameters = getBaseParameters("ListDomains");
        if (moreToken != null && moreToken.length() > 0) {
            parameters.put(MORETOKEN_STRING, moreToken.toString());
        }
        if (maxResults != null) {
            parameters.put("MaxResults",maxResults.toString());
        }
        Response response = invoke(parameters);
        if (moreToken != null) {
            moreToken.setLength(0);
            moreToken.append(response.moreToken);
        }
                                                                                                                                                      
        Element rootElement = (Element) response.rootElement;
        NodeList domainNodes = rootElement.getElementsByTagName("DomainName");
        List<String> domains = new ArrayList<String>();
        for (int i = 0; i < domainNodes.getLength(); ++i) {
            Node domainNode = domainNodes.item(i);
            Node domainNameNode = domainNode.getFirstChild();
            String domainName = domainNameNode.getNodeValue();
            domains.add(domainName);
        }

        //
        NodeList moreTokenNodes = rootElement.getElementsByTagName(MORETOKEN_STRING);
        if (moreTokenNodes.getLength() > 0) {
            Element moreTokenElement = (Element) moreTokenNodes.item(0);
            moreTokenElement.normalize();
            Node tokenNode = moreTokenElement.getFirstChild();
            String newMoreToken = tokenNode.getNodeValue();
            if (moreToken != null && (newMoreToken.compareToIgnoreCase("null") == 0 || newMoreToken.length() == 0)) {
                moreToken.setLength(0);
            }
        }
        return domains;
    }


    public void deleteAttributes(String domain, String identifier) {
        deleteAttributes(domain, identifier, new HashMap<String, String>());
    }
                                                                                                                                                      
    /**
     * Delete specified atrributes and/or values from item
     *
     * @param   attributes      set of attributes
     * @return                  result of the operation
     */
    public void deleteAttributes(String domain, String identifier, Map<String, String> attributes) {
        Map<String,String> parameters = getBaseParameters("DeleteAttributes");
        parameters.put("ItemName",identifier);
        parameters.put("DomainName",domain);
        if (attributes != null) {
            int i = 0;
            for (Map.Entry<String, String> attr : attributes.entrySet()) {
                String name = "Attribute."+ i + ".Name";
                parameters.put(name,attr.getKey());
                if (attr.getValue() != null) {
                    String value = "Attribute."+ i + ".Value";
                    parameters.put(value, attr.getValue());
                }
                ++i;
            }
        }
        invoke(parameters);
    }


    public void putAttributes(String domain, String identifier, Map<String, String> attributes) {
        putAttributes(domain, identifier, attributes, true);
    }


    public void putAttributes(String domain, String identifier, Map<String, String> attributes, boolean replace) {
        Map<String,String> parameters = getBaseParameters("PutAttributes");
        parameters.put("ItemName", identifier);
        parameters.put("DomainName", domain);
        int i = 0;
        if (attributes.size() > 255) throw new IllegalArgumentException("more than 255 attributes not supported");
        System.out.println("Writing " + attributes.size() + " attributes for " + identifier);
        for (Map.Entry<String, String> attr : attributes.entrySet()) {
            if (attr.getKey().length() > 700) throw new IllegalArgumentException("name '" + attr.getKey() + "' more than 700 bytes");
            if (attr.getValue().length() > 700) throw new IllegalArgumentException("value '" + attr.getValue() + "' of name '" + attr.getKey() + "' more than 700 bytes");
            String name = "Attribute."+ i + ".Name";
            parameters.put(name,attr.getKey());
            String value = "Attribute."+ i + ".Value";
            parameters.put(value, attr.getValue());
            i++;
        }
        //
        if (replace) {
            parameters.put("Replace","true");
        }
        invoke(parameters);
   }
   
    public void replaceAttributes(String domain, String identifier, Map<String, String> attributes) {
         putAttributes(domain, identifier, attributes,true);
    }


    public Map<String, String> getAttributes(String domain, String identifier){
        return getAttributes(domain, identifier, new HashMap<String, String>());
    }

    public Map<String, String> getAttributes(String domain, String identifier, Map<String, String> attributes) {
        String NAME_STRING = "Name";
        String VALUE_STRING = "Value";
        String ATTRIBUTE_STRING = "Attribute";
                                                                                                                                                      
        Map<String,String> parameters =  getBaseParameters("GetAttributes");
        parameters.put("ItemName", identifier);
        parameters.put("DomainName",domain);

        int k = 0;
        for (Map.Entry<String, String> attr : attributes.entrySet()) {
            String name = "AttributeName."+k;
            parameters.put(name,attr.getKey());
            ++k;
        }

        //
        Response response =  invoke(parameters);
        Element rootElement = (Element) response.rootElement;
        NodeList attributeNodes = rootElement.getElementsByTagName(ATTRIBUTE_STRING);
        Map<String, String> returnAttributes = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
        //
        for (int i = 0; i < attributeNodes.getLength(); ++i) {
            Node attributeNode = attributeNodes.item(i);
                                                                                                                                                      
            Element attributeElement = (Element)attributeNode;
                                                                                                                                                      
            NodeList nameNodes = attributeElement.getElementsByTagName(NAME_STRING);
            Node nameNode = nameNodes.item(0);
            nameNode.normalize();
            Node attributeNameNode = nameNode.getFirstChild();
            String attributeName = attributeNameNode.getNodeValue();
            NodeList valueNodes = attributeElement.getElementsByTagName(VALUE_STRING);
            Node valueNode = valueNodes.item(0);
            valueNode.normalize();
            Node attributeValueNode = valueNode.getFirstChild();
            String attributeValue = attributeValueNode.getNodeValue();
            returnAttributes.put(attributeName,attributeValue);
        }
        return returnAttributes;
    }


    protected Response invoke(Map<String,String> parameters) {
        try {
            Document document = getResponse(parameters); 
            Element rootElement = document.getDocumentElement();
            Element responseStatus = (Element) rootElement.getElementsByTagName("ResponseStatus").item(0);
            Node requestIdNode = responseStatus.getElementsByTagName("RequestId").item(0);
            String requestId = null;
            if (requestIdNode != null && requestIdNode.getFirstChild() != null) {
                requestId = requestIdNode.getFirstChild().getNodeValue();
            }
            String boxUsage = null;
            Node boxUsagesNode = responseStatus.getElementsByTagName("BoxUsage").item(0);
            if (boxUsagesNode != null && boxUsagesNode.getFirstChild() != null) {
                boxUsage = boxUsagesNode.getFirstChild().getNodeValue();
            }
            return new Response(document, rootElement, requestId, boxUsage, null, null);
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    private static String getSignature(String data, String key) throws NoSuchAlgorithmException, InvalidKeyException {
        SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(),HMAC_SHA1_ALGORITHM);
        Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
        mac.init(signingKey);
        byte[] rawHmac = mac.doFinal(data.getBytes());
        return new sun.misc.BASE64Encoder().encode(rawHmac);
    }

    private Map<String,String> getBaseParameters(String action) {
        Collator myCollator = Collator.getInstance();
        // Note that these parameters must be sent in order
        Map<String,String> baseParameters = new TreeMap<String,String>(myCollator);
        baseParameters.put("Action", action);
        baseParameters.put("Version", version);
        baseParameters.put("AWSAccessKeyId", accessId);
        baseParameters.put("SignatureVersion", "1");
                                                                                                                                                      
        Date curTime = new Date();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
        StringBuffer timeBuffer = new StringBuffer();
        FieldPosition fp = new FieldPosition(NumberFormat.INTEGER_FIELD);
        dateFormat.format(curTime, timeBuffer, fp);
        timeBuffer.insert(timeBuffer.length()-2, ":");
                                                                                                                                                      
        baseParameters.put("Timestamp",timeBuffer.toString());
        return baseParameters;
    }
  
    private Document getResponse(Map<String,String> parameters) throws UnsupportedEncodingException, MalformedURLException, IOException, NoSuchAlgorithmException, SAXException, ParserConfigurationException, InvalidKeyException {
        String path = uri + getUrlPath(parameters);
        URL url = new URL(path);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setDoOutput(true);
        connection.setRequestMethod("GET");
        connection.connect();

        System.out.println("Connecting to " + url);
        int respCode = connection.getResponseCode();

        if (respCode == HttpURLConnection.HTTP_OK) {
            InputSource inputSource = new InputSource(new BufferedReader(new InputStreamReader(connection.getInputStream())));
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
            return documentBuilder.parse(inputSource);
        } else {
            InputSource inputSource = new InputSource(new BufferedReader(new InputStreamReader(connection.getErrorStream())));
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
            Document document = documentBuilder.parse(inputSource);
            Element rootElement = document.getDocumentElement();
            Node requestIdNode = rootElement.getElementsByTagName("RequestID").item(0);
            StringBuilder errorMessage = new StringBuilder("Failed to invoke input request '" + path + "'");

            //
            if (requestIdNode != null && requestIdNode.getFirstChild() != null) {
                errorMessage.append(" with requestId " + requestIdNode.getFirstChild().getNodeValue());
                Element errors = (Element) rootElement.getElementsByTagName("Errors").item(0);
                if (errors != null) {
                    Element error = (Element) errors.getElementsByTagName("Error").item(0);
                    if (error != null) {
                        Node messageNode = error.getElementsByTagName("Message").item(0);
                        if (messageNode != null) errorMessage.append(" due to " + messageNode.getFirstChild().getNodeValue());
                    }
                }
            }
            throw new RuntimeException(errorMessage.toString());
        }
    }


   private String encode(String name){
        try {
             return URLEncoder.encode(name, DEFAULT_ENCODING);
        } catch (UnsupportedEncodingException ex) {
            throw new RuntimeException("Failed to encode '" + name + "'");
        }
    }
                                                                                                                                                  

    private String getUrlPath(Map<String,String> parameters) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException {
        StringBuilder queryString = new StringBuilder("?");
        StringBuilder signatureData = new StringBuilder();
        boolean first = true;
        for (Map.Entry<String, String> param : parameters.entrySet()) {
            if (first) {
                first = false;
            } else {
                queryString.append("&");
            }
                                                                                                                                                      
            signatureData.append(param.getKey() + param.getValue());
            queryString.append(param.getKey() + "=" + URLEncoder.encode((String)param.getValue(), DEFAULT_ENCODING));
        }

        queryString.append("&Signature=" + URLEncoder.encode(getSignature(signatureData.toString(),secretKey), DEFAULT_ENCODING)); 
        return queryString.toString();
    }
}
