package org.springframework.security.web.authentication.www;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.security.crypto.codec.Hex;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

final class DigestAuthUtils {

    private static final String[] EMPTY_STRING_ARRAY = new String[0];

    static String encodePasswordInA1Format(String username, String realm, String password) {
        String a1 = username + ":" + realm + ":" + password;

        return md5Hex(a1);
    }

    static String[] splitIgnoringQuotes(String str, char separatorChar) {
        if (str == null) {
            return null;
        }

        int len = str.length();

        if (len == 0) {
            return EMPTY_STRING_ARRAY;
        }

        List<String> list = new ArrayList<String>();
        int i = 0;
        int start = 0;
        boolean match = false;

        while (i < len) {
            if (str.charAt(i) == '"') {
                i++;
                while (i < len) {
                    if (str.charAt(i) == '"') {
                        i++;
                        break;
                    }
                    i++;
                }
                match = true;
                continue;
            }
            if (str.charAt(i) == separatorChar) {
                if (match) {
                    list.add(str.substring(start, i));
                    match = false;
                }
                start = ++i;
                continue;
            }
            match = true;
            i++;
        }
        if (match) {
            list.add(str.substring(start, i));
        }

        return list.toArray(new String[list.size()]);
    }

    /**
     * Computes the <code>response</code> portion of a Digest authentication header. Both the server and user
     * agent should compute the <code>response</code> independently. Provided as a static method to simplify the
     * coding of user agents.
     *
     * @param passwordAlreadyEncoded true if the password argument is already encoded in the correct format. False if
     *                               it is plain text.
     * @param username               the user's login name.
     * @param realm                  the name of the realm.
     * @param password               the user's password in plaintext or ready-encoded.
     * @param httpMethod             the HTTP request method (GET, POST etc.)
     * @param uri                    the request URI.
     * @param qop                    the qop directive, or null if not set.
     * @param nonce                  the nonce supplied by the server
     * @param nc                     the "nonce-count" as defined in RFC 2617.
     * @param cnonce                 opaque string supplied by the client when qop is set.
     * @return the MD5 of the digest authentication response, encoded in hex
     * @throws IllegalArgumentException if the supplied qop value is unsupported.
     */
    static String generateDigest(boolean passwordAlreadyEncoded, String username, String realm, String password,
                                        String httpMethod, String uri, String qop, String nonce, String nc, String cnonce)
            throws IllegalArgumentException {
        String a1Md5;
        String a2 = httpMethod + ":" + uri;
        String a2Md5 = md5Hex(a2);

        if (passwordAlreadyEncoded) {
            a1Md5 = password;
        } else {
            a1Md5 = DigestAuthUtils.encodePasswordInA1Format(username, realm, password);
        }

        String digest;

        if (qop == null) {
            // as per RFC 2069 compliant clients (also reaffirmed by RFC 2617)
            digest = a1Md5 + ":" + nonce + ":" + a2Md5;
        } else if ("auth".equals(qop)) {
            // As per RFC 2617 compliant clients
            digest = a1Md5 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + a2Md5;
        } else {
            throw new IllegalArgumentException("This method does not support a qop: '" + qop + "'");
        }

        return md5Hex(digest);
    }

    /**
     * Takes an array of <code>String</code>s, and for each element removes any instances of
     * <code>removeCharacter</code>, and splits the element based on the <code>delimiter</code>. A <code>Map</code> is
     * then generated, with the left of the delimiter providing the key, and the right of the delimiter providing the
     * value.<p>Will trim both the key and value before adding to the <code>Map</code>.</p>
     *
     * @param array            the array to process
     * @param delimiter        to split each element using (typically the equals symbol)
     * @param removeCharacters one or more characters to remove from each element prior to attempting the split
     *                         operation (typically the quotation mark symbol) or <code>null</code> if no removal should occur
     * @return a <code>Map</code> representing the array contents, or <code>null</code> if the array to process was
     *         null or empty
     */
    static Map<String, String> splitEachArrayElementAndCreateMap(String[] array, String delimiter, String removeCharacters) {
        if ((array == null) || (array.length == 0)) {
            return null;
        }

        Map<String, String> map = new HashMap<String, String>();

        for (String s : array) {
            String postRemove;

            if (removeCharacters == null) {
                postRemove = s;
            } else {
                postRemove = StringUtils.replace(s, removeCharacters, "");
            }

            String[] splitThisArrayElement = split(postRemove, delimiter);

            if (splitThisArrayElement == null) {
                continue;
            }

            map.put(splitThisArrayElement[0].trim(), splitThisArrayElement[1].trim());
        }

        return map;
    }


    /**
     * Splits a <code>String</code> at the first instance of the delimiter.<p>Does not include the delimiter in
     * the response.</p>
     *
     * @param toSplit   the string to split
     * @param delimiter to split the string up with
     * @return a two element array with index 0 being before the delimiter, and index 1 being after the delimiter
     *         (neither element includes the delimiter)
     * @throws IllegalArgumentException if an argument was invalid
     */
    static String[] split(String toSplit, String delimiter) {
        Assert.hasLength(toSplit, "Cannot split a null or empty string");
        Assert.hasLength(delimiter, "Cannot use a null or empty delimiter to split a string");

        if (delimiter.length() != 1) {
            throw new IllegalArgumentException("Delimiter can only be one character in length");
        }

        int offset = toSplit.indexOf(delimiter);

        if (offset < 0) {
            return null;
        }

        String beforeDelimiter = toSplit.substring(0, offset);
        String afterDelimiter = toSplit.substring(offset + 1);

        return new String[]{beforeDelimiter, afterDelimiter};
    }

    static String md5Hex(String data) {
        MessageDigest digest;
        try {
            digest = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("No MD5 algorithm available!");
        }

        return new String(Hex.encode(digest.digest(data.getBytes())));
    }
}
