/**
 * Copyright Microsoft Corporation
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.microsoft.windowsazure.services.blob.implementation;

import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;

import javax.inject.Named;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.microsoft.windowsazure.core.RFC1123DateConverter;
import com.microsoft.windowsazure.core.pipeline.jersey.EntityStreamingListener;
import com.microsoft.windowsazure.services.blob.BlobConfiguration;
import com.microsoft.windowsazure.services.blob.implementation.SharedKeyUtils.QueryParam;
import com.sun.jersey.api.client.ClientRequest;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.filter.ClientFilter;

public class SharedKeyFilter extends ClientFilter implements
        EntityStreamingListener {
    private static Log log = LogFactory.getLog(SharedKeyFilter.class);

    private final String accountName;
    private final HmacSHA256Sign signer;

    public SharedKeyFilter(
            @Named(BlobConfiguration.ACCOUNT_NAME) String accountName,
            @Named(BlobConfiguration.ACCOUNT_KEY) String accountKey) {
        this.accountName = accountName;
        this.signer = new HmacSHA256Sign(accountKey);
    }

    protected String getHeader(ClientRequest cr, String headerKey) {
        return SharedKeyUtils.getHeader(cr, headerKey);
    }

    protected HmacSHA256Sign getSigner() {
        return signer;
    }

    protected String getAccountName() {
        return accountName;
    }

    @Override
    public ClientResponse handle(ClientRequest cr) {
        // Only sign if no other filter is handling authorization
        if (cr.getProperties().get(SharedKeyUtils.AUTHORIZATION_FILTER_MARKER) == null) {
            cr.getProperties().put(SharedKeyUtils.AUTHORIZATION_FILTER_MARKER,
                    null);

            // Register ourselves as listener so we are called back when the
            // entity is
            // written to the output stream by the next filter in line.
            if (cr.getProperties().get(EntityStreamingListener.class.getName()) == null) {
                cr.getProperties().put(EntityStreamingListener.class.getName(),
                        this);
            }

        }
        return this.getNext().handle(cr);
    }

    @Override
    public void onBeforeStreamingEntity(ClientRequest clientRequest) {
        // All headers should be known at this point, time to sign!
        sign(clientRequest);
    }

    /*
     * StringToSign = VERB + "\n" + Content-Encoding + "\n" Content-Language +
     * "\n" Content-Length + "\n" Content-MD5 + "\n" + Content-Type + "\n" +
     * Date + "\n" + If-Modified-Since + "\n" If-Match + "\n" If-None-Match +
     * "\n" If-Unmodified-Since + "\n" Range + "\n" CanonicalizedHeaders +
     * CanonicalizedResource;
     */
    public void sign(ClientRequest cr) {
        // gather signed material
        addOptionalDateHeader(cr);

        // build signed string
        String stringToSign = cr.getMethod() + "\n"
                + getHeader(cr, "Content-Encoding") + "\n"
                + getHeader(cr, "Content-Language") + "\n"
                + getHeader(cr, "Content-Length") + "\n"
                + getHeader(cr, "Content-MD5") + "\n"
                + getHeader(cr, "Content-Type") + "\n" + getHeader(cr, "Date")
                + "\n" + getHeader(cr, "If-Modified-Since") + "\n"
                + getHeader(cr, "If-Match") + "\n"
                + getHeader(cr, "If-None-Match") + "\n"
                + getHeader(cr, "If-Unmodified-Since") + "\n"
                + getHeader(cr, "Range") + "\n";

        stringToSign += getCanonicalizedHeaders(cr);
        stringToSign += getCanonicalizedResource(cr);

        if (log.isDebugEnabled()) {
            log.debug(String.format("String to sign: \"%s\"", stringToSign));
        }
        // System.out.println(String.format("String to sign: \"%s\"",
        // stringToSign));

        String signature = this.signer.sign(stringToSign);
        cr.getHeaders().putSingle("Authorization",
                "SharedKey " + this.accountName + ":" + signature);
    }

    protected void addOptionalDateHeader(ClientRequest cr) {
        String date = getHeader(cr, "Date");
        if (date == "") {
            date = new RFC1123DateConverter().format(new Date());
            cr.getHeaders().putSingle("Date", date);
        }
    }

    /**
     * Constructing the Canonicalized Headers String
     * 
     * To construct the CanonicalizedHeaders portion of the signature string,
     * follow these steps:
     * 
     * 1. Retrieve all headers for the resource that begin with x-ms-, including
     * the x-ms-date header.
     * 
     * 2. Convert each HTTP header name to lowercase.
     * 
     * 3. Sort the headers lexicographically by header name, in ascending order.
     * Note that each header may appear only once in the string.
     * 
     * 4. Unfold the string by replacing any breaking white space with a single
     * space.
     * 
     * 5. Trim any white space around the colon in the header.
     * 
     * 6. Finally, append a new line character to each canonicalized header in
     * the resulting list. Construct the CanonicalizedHeaders string by
     * concatenating all headers in this list into a single string.
     */
    private String getCanonicalizedHeaders(ClientRequest cr) {
        return SharedKeyUtils.getCanonicalizedHeaders(cr);
    }

    /**
     * This format supports Shared Key authentication for the 2009-09-19 version
     * of the Blob and Queue services. Construct the CanonicalizedResource
     * string in this format as follows:
     * 
     * 1. Beginning with an empty string (""), append a forward slash (/),
     * followed by the name of the account that owns the resource being
     * accessed.
     * 
     * 2. Append the resource's encoded URI path, without any query parameters.
     * 
     * 3. Retrieve all query parameters on the resource URI, including the comp
     * parameter if it exists.
     * 
     * 4. Convert all parameter names to lowercase.
     * 
     * 5. Sort the query parameters lexicographically by parameter name, in
     * ascending order.
     * 
     * 6. URL-decode each query parameter name and value.
     * 
     * 7. Append each query parameter name and value to the string in the
     * following format, making sure to include the colon (:) between the name
     * and the value:
     * 
     * parameter-name:parameter-value
     * 
     * 8. If a query parameter has more than one value, sort all values
     * lexicographically, then include them in a comma-separated list:
     * 
     * parameter-name:parameter-value-1,parameter-value-2,parameter-value-n
     * 
     * 9. Append a new line character (\n) after each name-value pair.
     */
    private String getCanonicalizedResource(ClientRequest cr) {
        // 1. Beginning with an empty string (""), append a forward slash (/),
        // followed by the name of the account that owns
        // the resource being accessed.
        String result = "/" + this.accountName;

        // 2. Append the resource's encoded URI path, without any query
        // parameters.
        result += cr.getURI().getPath();

        // 3. Retrieve all query parameters on the resource URI, including the
        // comp parameter if it exists.
        // 6. URL-decode each query parameter name and value.
        List<QueryParam> queryParams = SharedKeyUtils.getQueryParams(cr
                .getURI().getQuery());

        // 4. Convert all parameter names to lowercase.
        for (QueryParam param : queryParams) {
            param.setName(param.getName().toLowerCase(Locale.US));
        }

        // 5. Sort the query parameters lexicographically by parameter name, in
        // ascending order.
        Collections.sort(queryParams);

        // 7. Append each query parameter name and value to the string
        // 8. If a query parameter has more than one value, sort all values
        // lexicographically, then include them in a comma-separated list
        for (int i = 0; i < queryParams.size(); i++) {
            QueryParam param = queryParams.get(i);

            List<String> values = param.getValues();
            // Collections.sort(values);

            // 9. Append a new line character (\n) after each name-value pair.
            result += "\n";
            result += param.getName();
            result += ":";
            for (int j = 0; j < values.size(); j++) {
                if (j > 0) {
                    result += ",";
                }
                result += values.get(j);
            }
        }

        return result;
    }
}
