/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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 org.apache.mina.proxy.utils;

import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.security.sasl.AuthenticationException;
import javax.security.sasl.SaslException;

/**
 * StringUtilities.java - Various methods to handle strings.
 * 
 * @author <a href="http://mina.apache.org">Apache MINA Project</a>
 * @since MINA 2.0.0-M3
 */
public class StringUtilities {
    private StringUtilities(){
    }
    
    /**
     * A directive is a parameter of the digest authentication process.
     * Returns the value of a directive from the map. If mandatory is true and the 
     * value is null, then it throws an {@link AuthenticationException}.
     *  
     * @param directivesMap the directive's map 
     * @param directive the name of the directive we want to retrieve
     * @param mandatory is the directive mandatory
     * @return the mandatory value as a String
     * @throws AuthenticationException if mandatory is true and if 
     * directivesMap.get(directive) == null
     */
    public static String getDirectiveValue(Map<String, String> directivesMap, String directive, boolean mandatory)
            throws AuthenticationException {
        String value = directivesMap.get(directive);
        
        if (value == null) {
            if (mandatory) {
                throw new AuthenticationException("\"" + directive + "\" mandatory directive is missing");
            }

            return "";
        }

        return value;
    }

    /**
     * Copy the directive to the {@link StringBuilder} if not null.
     * (A directive is a parameter of the digest authentication process.)
     * 
     * @param directives the directives map
     * @param sb the output buffer
     * @param directive the directive name to look for
     */
    public static void copyDirective(Map<String, String> directives, StringBuilder sb, String directive) {
        String directiveValue = directives.get(directive);
        
        if (directiveValue != null) {
            sb.append(directive).append(" = \"").append(directiveValue).append("\", ");
        }
    }

    /**
     * Copy the directive from the source map to the destination map, if it's
     * value isn't null.
     * (A directive is a parameter of the digest authentication process.)
     * 
     * @param src the source map
     * @param dst the destination map
     * @param directive the directive name
     * @return the value of the copied directive
     */
    public static String copyDirective(Map<String, String> src, Map<String, String> dst, String directive) {
        String directiveValue = src.get(directive);
        
        if (directiveValue != null) {
            dst.put(directive, directiveValue);
        }

        return directiveValue;
    }

    /**
     * Parses digest-challenge string, extracting each token and value(s). Each token
     * is a directive.
     *
     * @param buf A non-null digest-challenge string.
     * @return A Map containing the aprsed directives
     * @throws SaslException if the String cannot be parsed according to RFC 2831
     */
    public static Map<String, String> parseDirectives(byte[] buf) throws SaslException {
        Map<String, String> map = new HashMap<>();
        boolean gettingKey = true;
        boolean gettingQuotedValue = false;
        boolean expectSeparator = false;
        byte bch;

        ByteArrayOutputStream key = new ByteArrayOutputStream(10);
        ByteArrayOutputStream value = new ByteArrayOutputStream(10);

        int i = skipLws(buf, 0);
        
        while (i < buf.length) {
            bch = buf[i];

            if (gettingKey) {
                if (bch == ',') {
                    if (key.size() != 0) {
                        throw new SaslException("Directive key contains a ',':" + key);
                    }

                    // Empty element, skip separator and lws
                    i = skipLws(buf, i + 1);
                } else if (bch == '=') {
                    if (key.size() == 0) {
                        throw new SaslException("Empty directive key");
                    }

                    gettingKey = false; // Termination of key
                    i = skipLws(buf, i + 1); // Skip to next non whitespace

                    // Check whether value is quoted
                    if (i < buf.length) {
                        if (buf[i] == '"') {
                            gettingQuotedValue = true;
                            ++i; // Skip quote
                        }
                    } else {
                        throw new SaslException("Valueless directive found: " + key.toString());
                    }
                } else if (isLws(bch)) {
                    // LWS that occurs after key
                    i = skipLws(buf, i + 1);

                    // Expecting '='
                    if (i < buf.length) {
                        if (buf[i] != '=') {
                            throw new SaslException("'=' expected after key: " + key.toString());
                        }
                    } else {
                        throw new SaslException("'=' expected after key: " + key.toString());
                    }
                } else {
                    key.write(bch); // Append to key
                    ++i; // Advance
                }
            } else if (gettingQuotedValue) {
                // Getting a quoted value
                if (bch == '\\') {
                    // quoted-pair = "\" CHAR ==> CHAR
                    ++i; // Skip escape
                    
                    if (i < buf.length) {
                        value.write(buf[i]);
                        ++i; // Advance
                    } else {
                        // Trailing escape in a quoted value
                        throw new SaslException("Unmatched quote found for directive: " + key.toString()
                                + " with value: " + value.toString());
                    }
                } else if (bch == '"') {
                    // closing quote
                    ++i; // Skip closing quote
                    gettingQuotedValue = false;
                    expectSeparator = true;
                } else {
                    value.write(bch);
                    ++i; // Advance
                }
            } else if (isLws(bch) || bch == ',') {
                // Value terminated
                extractDirective(map, key.toString(), value.toString());
                key.reset();
                value.reset();
                gettingKey = true;
                gettingQuotedValue = expectSeparator = false;
                i = skipLws(buf, i + 1); // Skip separator and LWS
            } else if (expectSeparator) {
                throw new SaslException("Expecting comma or linear whitespace after quoted string: \""
                        + value.toString() + "\"");
            } else {
                value.write(bch); // Unquoted value
                ++i; // Advance
            }
        }

        if (gettingQuotedValue) {
            throw new SaslException("Unmatched quote found for directive: " + key.toString() + " with value: "
                    + value.toString());
        }

        // Get last pair
        if (key.size() > 0) {
            extractDirective(map, key.toString(), value.toString());
        }

        return map;
    }

    /**
     * Processes directive/value pairs from the digest-challenge and
     * fill out the provided map.
     * 
     * @param key A non-null String challenge token name.
     * @param value A non-null String token value.
     * @throws SaslException if either the key or the value is null or
     * if the key already has a value. 
     */
    private static void extractDirective(Map<String, String> map, String key, String value) throws SaslException {
        if (map.get(key) != null) {
            throw new SaslException("Peer sent more than one " + key + " directive");
        }

        map.put(key, value);
    }

    /**
     * Is character a linear white space ?
     * LWS            = [CRLF] 1*( SP | HT )
     * Note that we're checking individual bytes instead of CRLF
     * 
     * @param b the byte to check
     * @return <tt>true</tt> if it's a linear white space
     */
    public static boolean isLws(byte b) {
        switch (b) {
            case 13: // US-ASCII CR, carriage return
            case 10: // US-ASCII LF, line feed
            case 32: // US-ASCII SP, space
            case 9: // US-ASCII HT, horizontal-tab
            return true;
        }

        return false;
    }

    /**
     * Skip all linear white spaces
     * 
     * @param buf the buf which is being scanned for lws
     * @param start the offset to start at
     * @return the next position in buf which isn't a lws character
     */
    private static int skipLws(byte[] buf, int start) {
        int i;

        for (i = start; i < buf.length; i++) {
            if (!isLws(buf[i])) {
                return i;
            }
        }

        return i;
    }

    /**
     * Used to convert username-value, passwd or realm to 8859_1 encoding
     * if all chars in string are within the 8859_1 (Latin 1) encoding range.
     * 
     * @param str a non-null String
     * @return a non-null String containing the 8859_1 encoded string
     * @throws UnsupportedEncodingException if we weren't able to decode using the ISO 8859_1 encoding
     */
    public static String stringTo8859_1(String str) throws UnsupportedEncodingException {
        if (str == null) {
            return "";
        }

        return new String(str.getBytes("UTF8"), "8859_1");
    }

    /**
     * Returns the value of the named header. If it has multiple values
     * then an {@link IllegalArgumentException} is thrown
     * 
     * @param headers the http headers map
     * @param key the key of the header 
     * @return the value of the http header
     */
    public static String getSingleValuedHeader(Map<String, List<String>> headers, String key) {
        List<String> values = headers.get(key);

        if (values == null) {
            return null;
        }

        if (values.size() > 1) {
            throw new IllegalArgumentException("Header with key [\"" + key + "\"] isn't single valued !");
        }

        return values.get(0);
    }

    /**
     * Adds an header to the provided map of headers.
     * 
     * @param headers the http headers map
     * @param key the name of the new header to add
     * @param value the value of the added header
     * @param singleValued if true and the map already contains one value
     * then it is replaced by the new value. Otherwise it simply adds a new
     * value to this multi-valued header.
     */
    public static void addValueToHeader(Map<String, List<String>> headers, String key, String value,
            boolean singleValued) {
        List<String> values = headers.get(key);

        if (values == null) {
            values = new ArrayList<>(1);
            headers.put(key, values);
        }

        if (singleValued && values.size() == 1) {
            values.set(0, value);
        } else {
            values.add(value);
        }
    }
}