/**
 * Copyright 2005-2010 Noelios Technologies.
 * 
 * The contents of this file are subject to the terms of one of the following
 * open source licenses: LGPL 3.0 or LGPL 2.1 or CDDL 1.0 or EPL 1.0 (the
 * "Licenses"). You can select the license that you prefer but you may not use
 * this file except in compliance with one of these Licenses.
 * 
 * You can obtain a copy of the LGPL 3.0 license at
 * http://www.opensource.org/licenses/lgpl-3.0.html
 * 
 * You can obtain a copy of the LGPL 2.1 license at
 * http://www.opensource.org/licenses/lgpl-2.1.php
 * 
 * You can obtain a copy of the CDDL 1.0 license at
 * http://www.opensource.org/licenses/cddl1.php
 * 
 * You can obtain a copy of the EPL 1.0 license at
 * http://www.opensource.org/licenses/eclipse-1.0.php
 * 
 * See the Licenses for the specific language governing permissions and
 * limitations under the Licenses.
 * 
 * Alternatively, you can obtain a royalty free commercial license with less
 * limitations, transferable or non-transferable, directly at
 * http://www.noelios.com/products/restlet-engine
 * 
 * Restlet is a registered trademark of Noelios Technologies.
 */

package org.restlet.engine.http.header;

import java.io.IOException;
import java.util.Iterator;
import static org.restlet.engine.http.header.HeaderUtils.*;

import org.restlet.data.CharacterSet;
import org.restlet.data.ClientInfo;
import org.restlet.data.Encoding;
import org.restlet.data.Form;
import org.restlet.data.Language;
import org.restlet.data.MediaType;
import org.restlet.data.Metadata;
import org.restlet.data.Parameter;
import org.restlet.data.Preference;
import org.restlet.util.Series;

/**
 * Preference header reader. Works for character sets, encodings, languages or
 * media types.
 * 
 * @author Jerome Louvel
 */
public class PreferenceReader<T extends Metadata> extends
        HeaderReader<Preference<T>> {
    public static final int TYPE_CHARACTER_SET = 1;

    public static final int TYPE_ENCODING = 2;

    public static final int TYPE_LANGUAGE = 3;

    public static final int TYPE_MEDIA_TYPE = 4;

    /**
     * Parses character set preferences from a header.
     * 
     * @param acceptCharsetHeader
     *            The header to parse.
     * @param clientInfo
     *            The client info to update.
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public static void addCharacterSets(String acceptCharsetHeader,
            ClientInfo clientInfo) {
        if (acceptCharsetHeader != null) {
            // Implementation according to
            // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.2
            if (acceptCharsetHeader.length() == 0) {
                clientInfo.getAcceptedCharacterSets().add(
                        new Preference<CharacterSet>(CharacterSet.ISO_8859_1));
            } else {
                PreferenceReader pr = new PreferenceReader(
                        PreferenceReader.TYPE_CHARACTER_SET,
                        acceptCharsetHeader);
                pr.addValues(clientInfo.getAcceptedCharacterSets());
            }
        } else {
            clientInfo.getAcceptedCharacterSets().add(
                    new Preference(CharacterSet.ALL));
        }
    }

    /**
     * Parses encoding preferences from a header.
     * 
     * @param acceptEncodingHeader
     *            The header to parse.
     * @param clientInfo
     *            The client info to update.
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public static void addEncodings(String acceptEncodingHeader,
            ClientInfo clientInfo) {
        if (acceptEncodingHeader != null) {
            PreferenceReader pr = new PreferenceReader(
                    PreferenceReader.TYPE_ENCODING, acceptEncodingHeader);
            pr.addValues(clientInfo.getAcceptedEncodings());
        } else {
            clientInfo.getAcceptedEncodings().add(
                    new Preference(Encoding.IDENTITY));
        }
    }

    /**
     * Adds language preferences from a header.
     * 
     * @param acceptLanguageHeader
     *            The header to parse.
     * @param clientInfo
     *            The client info to update.
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public static void addLanguages(String acceptLanguageHeader,
            ClientInfo clientInfo) {
        if (acceptLanguageHeader != null) {
            PreferenceReader pr = new PreferenceReader(
                    PreferenceReader.TYPE_LANGUAGE, acceptLanguageHeader);
            pr.addValues(clientInfo.getAcceptedLanguages());
        } else {
            clientInfo.getAcceptedLanguages().add(new Preference(Language.ALL));
        }
    }

    /**
     * Parses media type preferences from a header.
     * 
     * @param acceptMediaTypeHeader
     *            The header to parse.
     * @param clientInfo
     *            The client info to update.
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public static void addMediaTypes(String acceptMediaTypeHeader,
            ClientInfo clientInfo) {
        if (acceptMediaTypeHeader != null) {
            PreferenceReader pr = new PreferenceReader(
                    PreferenceReader.TYPE_MEDIA_TYPE, acceptMediaTypeHeader);
            pr.addValues(clientInfo.getAcceptedMediaTypes());
        } else {
            clientInfo.getAcceptedMediaTypes().add(
                    new Preference(MediaType.ALL));
        }
    }

    /**
     * Parses a quality value.<br>
     * If the quality is invalid, an IllegalArgumentException is thrown.
     * 
     * @param quality
     *            The quality value as a string.
     * @return The parsed quality value as a float.
     */
    public static float readQuality(String quality) {
        try {
            float result = Float.valueOf(quality);

            if (PreferenceWriter.isValidQuality(result)) {
                return result;
            }

            throw new IllegalArgumentException(
                    "Invalid quality value detected. Value must be between 0 and 1.");
        } catch (NumberFormatException nfe) {
            throw new IllegalArgumentException(
                    "Invalid quality value detected. Value must be between 0 and 1.");
        }
    }

    /** The type of metadata read. */
    private volatile int type;

    /**
     * Constructor.
     * 
     * @param type
     *            The type of metadata read.
     * @param header
     *            The header to read.
     */
    public PreferenceReader(int type, String header) {
        super(header);
        this.type = type;
    }

    /**
     * Creates a new preference.
     * 
     * @param metadata
     *            The metadata name.
     * @param parameters
     *            The parameters list.
     * @return The new preference.
     */
    @SuppressWarnings("unchecked")
    protected Preference<T> createPreference(CharSequence metadata,
            Series<Parameter> parameters) {
        Preference<T> result;

        if (parameters == null) {
            result = new Preference<T>();

            switch (this.type) {
            case TYPE_CHARACTER_SET:
                result.setMetadata((T) CharacterSet.valueOf(metadata.toString()));
                break;

            case TYPE_ENCODING:
                result.setMetadata((T) Encoding.valueOf(metadata.toString()));
                break;

            case TYPE_LANGUAGE:
                result.setMetadata((T) Language.valueOf(metadata.toString()));
                break;

            case TYPE_MEDIA_TYPE:
                result.setMetadata((T) MediaType.valueOf(metadata.toString()));
                break;
            }
        } else {
            final Series<Parameter> mediaParams = extractMediaParams(parameters);
            final float quality = extractQuality(parameters);
            result = new Preference<T>(null, quality, parameters);

            switch (this.type) {
            case TYPE_CHARACTER_SET:
                result.setMetadata((T) new CharacterSet(metadata.toString()));
                break;

            case TYPE_ENCODING:
                result.setMetadata((T) new Encoding(metadata.toString()));
                break;

            case TYPE_LANGUAGE:
                result.setMetadata((T) new Language(metadata.toString()));
                break;

            case TYPE_MEDIA_TYPE:
                result.setMetadata((T) new MediaType(metadata.toString(),
                        mediaParams));
                break;
            }
        }

        return result;
    }

    /**
     * Extract the media parameters. Only leave as the quality parameter if
     * found. Modifies the parameters list.
     * 
     * @param parameters
     *            All the preference parameters.
     * @return The media parameters.
     */
    protected Series<Parameter> extractMediaParams(Series<Parameter> parameters) {
        Series<Parameter> result = null;
        boolean qualityFound = false;
        Parameter param = null;

        if (parameters != null) {
            result = new Form();

            for (final Iterator<Parameter> iter = parameters.iterator(); !qualityFound
                    && iter.hasNext();) {
                param = iter.next();

                if (param.getName().equals("q")) {
                    qualityFound = true;
                } else {
                    iter.remove();
                    result.add(param);
                }
            }
        }

        return result;
    }

    /**
     * Extract the quality value. If the value is not found, 1 is returned.
     * 
     * @param parameters
     *            The preference parameters.
     * @return The quality value.
     */
    protected float extractQuality(Series<Parameter> parameters) {
        float result = 1F;
        boolean found = false;

        if (parameters != null) {
            Parameter param = null;
            for (final Iterator<Parameter> iter = parameters.iterator(); !found
                    && iter.hasNext();) {
                param = iter.next();
                if (param.getName().equals("q")) {
                    result = readQuality(param.getValue());
                    found = true;

                    // Remove the quality parameter as we will directly store it
                    // in the Preference object
                    iter.remove();
                }
            }
        }

        return result;
    }

    /**
     * Read the next preference.
     * 
     * @return The next preference.
     */
    public Preference<T> readValue() throws IOException {
        Preference<T> result = null;

        boolean readingMetadata = true;
        boolean readingParamName = false;
        boolean readingParamValue = false;

        StringBuilder metadataBuffer = new StringBuilder();
        StringBuilder paramNameBuffer = null;
        StringBuilder paramValueBuffer = null;

        Series<Parameter> parameters = null;
        int next = 0;

        while (result == null) {
            next = read();

            if (readingMetadata) {
                if ((next == -1) || isComma(next)) {
                    if (metadataBuffer.length() > 0) {
                        // End of metadata section
                        // No parameters detected
                        result = createPreference(metadataBuffer, null);
                    } else {
                        // Ignore empty metadata name
                        break;
                    }
                } else if (next == ';') {
                    if (metadataBuffer.length() > 0) {
                        // End of metadata section
                        // Parameters detected
                        readingMetadata = false;
                        readingParamName = true;
                        paramNameBuffer = new StringBuilder();
                        parameters = new Form();
                    } else {
                        throw new IOException("Empty metadata name detected.");
                    }
                } else if (isSpace(next)) {
                    // Ignore spaces
                } else if (isText(next)) {
                    metadataBuffer.append((char) next);
                } else {
                    throw new IOException("Unexpected character \""
                            + (char) next + "\" detected.");
                }
            } else if (readingParamName) {
                if (next == '=') {
                    if (paramNameBuffer.length() > 0) {
                        // End of parameter name section
                        readingParamName = false;
                        readingParamValue = true;
                        paramValueBuffer = new StringBuilder();
                    } else {
                        throw new IOException("Empty parameter name detected.");
                    }
                } else if ((next == -1) || isComma(next)) {
                    if (paramNameBuffer.length() > 0) {
                        // End of parameters section
                        parameters.add(Parameter.create(paramNameBuffer, null));
                        result = createPreference(metadataBuffer, parameters);
                    } else {
                        throw new IOException("Empty parameter name detected.");
                    }
                } else if (next == ';') {
                    // End of parameter
                    parameters.add(Parameter.create(paramNameBuffer, null));
                    paramNameBuffer = new StringBuilder();
                    readingParamName = true;
                    readingParamValue = false;
                } else if (isSpace(next) && (paramNameBuffer.length() == 0)) {
                    // Ignore white spaces
                } else if (isTokenChar(next)) {
                    paramNameBuffer.append((char) next);
                } else {
                    throw new IOException("Unexpected character \""
                            + (char) next + "\" detected.");
                }
            } else if (readingParamValue) {
                if ((next == -1) || isComma(next) || isSpace(next)) {
                    if (paramValueBuffer.length() > 0) {
                        // End of parameters section
                        parameters.add(Parameter.create(paramNameBuffer,
                                paramValueBuffer));
                        result = createPreference(metadataBuffer, parameters);
                    } else {
                        throw new IOException("Empty parameter value detected");
                    }
                } else if (next == ';') {
                    // End of parameter
                    parameters.add(Parameter.create(paramNameBuffer,
                            paramValueBuffer));
                    paramNameBuffer = new StringBuilder();
                    readingParamName = true;
                    readingParamValue = false;
                } else if ((next == '"') && (paramValueBuffer.length() == 0)) {
                    // Parse the quoted string
                    boolean done = false;
                    boolean quotedPair = false;

                    while ((!done) && (next != -1)) {
                        next = read();

                        if (quotedPair) {
                            // End of quoted pair (escape sequence)
                            if (isText(next)) {
                                paramValueBuffer.append((char) next);
                                quotedPair = false;
                            } else {
                                throw new IOException(
                                        "Invalid character detected in quoted string. Please check your value");
                            }
                        } else if (isDoubleQuote(next)) {
                            // End of quoted string
                            done = true;
                        } else if (next == '\\') {
                            // Begin of quoted pair (escape sequence)
                            quotedPair = true;
                        } else if (isText(next)) {
                            paramValueBuffer.append((char) next);
                        } else {
                            throw new IOException(
                                    "Invalid character detected in quoted string. Please check your value");
                        }
                    }
                } else if (isTokenChar(next)) {
                    paramValueBuffer.append((char) next);
                } else {
                    throw new IOException("Unexpected character \""
                            + (char) next + "\" detected.");
                }
            }
        }

        if (isComma(next)) {
            // Unread character which isn't part of the value
            unread();
        }

        return result;
    }
}
