/*
 * 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 javax.mail.internet;

import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;// Represents lists in things like
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import org.apache.geronimo.mail.util.ASCIIUtil;
import org.apache.geronimo.mail.util.RFC2231Encoder;
import org.apache.geronimo.mail.util.SessionUtil;

// Content-Type: text/plain;charset=klingon
//
// The ;charset=klingon is the parameter list, may have more of them with ';'

/**
 * @version $Rev: 1637646 $ $Date: 2014-11-09 11:30:06 +0100 (So, 09. Nov 2014) $
 */
public class ParameterList {
    private static final String MIME_ENCODEPARAMETERS = "mail.mime.encodeparameters";
    private static final String MIME_DECODEPARAMETERS = "mail.mime.decodeparameters";
    private static final String MIME_DECODEPARAMETERS_STRICT = "mail.mime.decodeparameters.strict";

    private static final int HEADER_SIZE_LIMIT = 76;

    private final Map _parameters = new LinkedHashMap(); //predictable iteration order, see toString()

    private boolean encodeParameters = false;
    private boolean decodeParameters = false;
    private boolean decodeParametersStrict = false;

    public ParameterList() {
        // figure out how parameter handling is to be performed.
        getInitialProperties();
    }

    public ParameterList(String list) throws ParseException {
        // figure out how parameter handling is to be performed.
        getInitialProperties();
        // get a token parser for the type information
        HeaderTokenizer tokenizer = new HeaderTokenizer(list, HeaderTokenizer.MIME);
        while (true) {
            HeaderTokenizer.Token token = tokenizer.next();

            switch (token.getType()) {
                // the EOF token terminates parsing.
                case HeaderTokenizer.Token.EOF:
                    return;

                // each new parameter is separated by a semicolon, including the first, which separates
                // the parameters from the main part of the header.
                case ';':
                    // the next token needs to be a parameter name
                    token = tokenizer.next();
                    // allow a trailing semicolon on the parameters.
                    if (token.getType() == HeaderTokenizer.Token.EOF) {
                        return;
                    }

                    if (token.getType() != HeaderTokenizer.Token.ATOM) {
                        throw new ParseException("Invalid parameter name: " + token.getValue());
                    }

                    // get the parameter name as a lower case version for better mapping.
                    String name = token.getValue().toLowerCase();

                    token = tokenizer.next();

                    // parameters are name=value, so we must have the "=" here.
                    if (token.getType() != '=') {
                        throw new ParseException("Missing '='");
                    }

                    // now the value, which may be an atom or a literal
                    token = tokenizer.next();

                    if (token.getType() != HeaderTokenizer.Token.ATOM && token.getType() != HeaderTokenizer.Token.QUOTEDSTRING) {
                        throw new ParseException("Invalid parameter value: " + token.getValue());
                    }

                    String value = token.getValue();
                    String decodedValue = null;

                    // we might have to do some additional decoding.  A name that ends with "*"
                    // is marked as being encoded, so if requested, we decode the value.
                    if (decodeParameters && name.endsWith("*")) {
                        // the name needs to be pruned of the marker, and we need to decode the value.
                        name = name.substring(0, name.length() - 1);
                        // get a new decoder
                        RFC2231Encoder decoder = new RFC2231Encoder(HeaderTokenizer.MIME);

                        try {
                            // decode the value
                            decodedValue = decoder.decode(value);
                        } catch (Exception e) {
                            // if we're doing things strictly, then raise a parsing exception for errors.
                            // otherwise, leave the value in its current state.
                            if (decodeParametersStrict) {
                                throw new ParseException("Invalid RFC2231 encoded parameter");
                            }
                        }
                        _parameters.put(name, new ParameterValue(name, decodedValue, value));
                    }
                    else {
                        _parameters.put(name, new ParameterValue(name, value));
                    }

                    break;

                default:
                    throw new ParseException("Missing ';'");

            }
        }
    }

    /**
     * Get the initial parameters that control parsing and values.
     * These parameters are controlled by System properties.
     */
    private void getInitialProperties() {
        decodeParameters = SessionUtil.getBooleanProperty(MIME_DECODEPARAMETERS, false);
        decodeParametersStrict = SessionUtil.getBooleanProperty(MIME_DECODEPARAMETERS_STRICT, false);
        encodeParameters = SessionUtil.getBooleanProperty(MIME_ENCODEPARAMETERS, true);
    }

    public int size() {
        return _parameters.size();
    }

    public String get(String name) {
        ParameterValue value = (ParameterValue)_parameters.get(name.toLowerCase());
        if (value != null) {
            return value.value;
        }
        return null;
    }

    public void set(String name, String value) {
        name = name.toLowerCase();
        _parameters.put(name, new ParameterValue(name, value));
    }

    public void set(String name, String value, String charset) {
        name = name.toLowerCase();
        // only encode if told to and this contains non-ASCII charactes.
        if (encodeParameters && !ASCIIUtil.isAscii(value)) {
            ByteArrayOutputStream out = new ByteArrayOutputStream();

            try {
                RFC2231Encoder encoder = new RFC2231Encoder(HeaderTokenizer.MIME);

                // extract the bytes using the given character set and encode
                byte[] valueBytes = value.getBytes(MimeUtility.javaCharset(charset));

                // the string format is charset''data
                out.write(charset.getBytes("ISO8859-1"));
                out.write('\'');
                out.write('\'');
                encoder.encode(valueBytes, 0, valueBytes.length, out);

                // default in case there is an exception
                _parameters.put(name, new ParameterValue(name, value, new String(out.toByteArray(), "ISO8859-1")));
                return;

            } catch (Exception e) {
                // just fall through and set the value directly if there is an error
            }
        }
        // default in case there is an exception
        _parameters.put(name, new ParameterValue(name, value));
    }

    public void remove(String name) {
        _parameters.remove(name);
    }

    public Enumeration getNames() {
        return Collections.enumeration(_parameters.keySet());
    }

    public String toString() {
        // we need to perform folding, but out starting point is 0.
        return toString(0);
    }

    public String toString(int used) {
        StringBuffer stringValue = new StringBuffer();

        Iterator entries = _parameters.entrySet().iterator();

        while (entries.hasNext()) {
            Map.Entry entry = (Map.Entry) entries.next();
            ParameterValue parm = (ParameterValue) entry.getValue();
            // get the values we're going to encode in here.
            String name = parm.getEncodedName();
            String value = parm.toString();

            // add the semicolon separator.  We also add a blank so that folding/unfolding rules can be used.
            stringValue.append("; ");
            used += 2;

            // too big for the current header line?
            if ((used + name.length() + value.length() + 1) > HEADER_SIZE_LIMIT) {
                // and a CRLF-combo combo.
                stringValue.append("\r\n\t");
                // reset the counter for a fresh line
                // note we use use 8 because we're using a rather than a blank
                used = 8;
            }
            // now add the keyword/value pair.
            stringValue.append(name);
            stringValue.append("=");

            used += name.length() + 1;

            // we're not out of the woods yet.  It is possible that the keyword/value pair by itself might
            // be too long for a single line.  If that's the case, the we need to fold the value, if possible
            if (used + value.length() > HEADER_SIZE_LIMIT) {
                String foldedValue = MimeUtility.fold(used, value);

                stringValue.append(foldedValue);

                // now we need to sort out how much of the current line is in use.
                int lastLineBreak = foldedValue.lastIndexOf('\n');

                if (lastLineBreak != -1) {
                    used = foldedValue.length() - lastLineBreak + 1;
                }
                else {
                    used += foldedValue.length();
                }
            }
            else {
                // no folding required, just append.
                stringValue.append(value);
                used += value.length();
            }
        }

        return stringValue.toString();
    }


    /**
     * Utility class for representing parameter values in the list.
     */
    class ParameterValue {
        public String name;              // the name of the parameter
        public String value;             // the original set value
        public String encodedValue;      // an encoded value, if encoding is requested.

        public ParameterValue(String name, String value) {
            this.name = name;
            this.value = value;
            this.encodedValue = null;
        }

        public ParameterValue(String name, String value, String encodedValue) {
            this.name = name;
            this.value = value;
            this.encodedValue = encodedValue;
        }

        public String toString() {
            if (encodedValue != null) {
                return MimeUtility.quote(encodedValue, HeaderTokenizer.MIME);
            }
            return MimeUtility.quote(value, HeaderTokenizer.MIME);
        }

        public String getEncodedName() {
            if (encodedValue != null) {
                return name + "*";
            }
            return name;
        }
    }
}
