/*
 * fred - MediaType.java - Copyright © 2011 David Roden
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package freenet.support;

import java.net.MalformedURLException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;

import freenet.client.DefaultMIMETypes;

/**
 * A media type denotes the content type of a document. A media consists of a
 * top-level type, a subtype, and an optional list of key-value pairs. An
 * example would be “audio/ogg” or “text/html; charset=utf-8.”
 * <p>
 * {@link MediaType}s are immutable. The setter methods (e.g.
 * {@link #setType(String)} and {@link #setSubtype(String)}) return new
 * {@link MediaType} objects with the requested part changed and all other parts
 * copied.
 * <p>
 * Media types are defined in <a href="http://www.ietf.org/rfc/rfc2046.txt">RFC
 * 2046</a>.
 *
 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
 */
public class MediaType {

	/** The top-level type. */
	private final String type;

	/** The subtype. */
	private final String subtype;

	/** The parameters. */
	private final LinkedHashMap<String, String> parameters = new LinkedHashMap<String, String>();

	/**
	 * Creates a new media type by parsing the given string.
	 *
	 * @param mediaType
	 *            The media type to parse
	 * @throws NullPointerException
	 *             if {@code mediaType} is {@code null}
	 * @throws MalformedURLException
	 *             if {@code mediaType} is incorrectly formatted, i.e. does not
	 *             contain a slash, or a parameter does not contain an equals
	 *             sign
	 */
	public MediaType(String mediaType) throws NullPointerException, MalformedURLException {
		if (mediaType == null) {
			throw new NullPointerException("contentType must not be null");
		}
		if(!DefaultMIMETypes.isPlausibleMIMEType(mediaType))
		    throw new MalformedURLException("Doesn't look like a MIME type");
		int slash = mediaType.indexOf('/');
		if (slash == -1) {
			throw new MalformedURLException("mediaType does not contain ‘/’!");
		}
		type = mediaType.substring(0, slash);
		int semicolon = mediaType.indexOf(';');
		if (semicolon == -1) {
			subtype = mediaType.substring(slash + 1);
			return;
		}
		subtype = mediaType.substring(slash + 1, semicolon).trim();
		String[] parameters = mediaType.substring(semicolon + 1).split(";");
		for (String parameter : parameters) {
			int equals = parameter.indexOf('=');
			if (equals == -1) {
				throw new MalformedURLException(String.format("Illegal parameter: “%s”", parameter));
			}
			String name = parameter.substring(0, equals).trim().toLowerCase();
			String value = parameter.substring(equals + 1).trim();
			if(value.startsWith("\"") && value.endsWith("\""))
			    value = value.substring(1, value.length()-1).trim();
			this.parameters.put(name, value);
		}
	}

	/**
	 * Creates a new media type.
	 *
	 * @param type
	 *            The top-level type
	 * @param subtype
	 *            The subtype
	 * @param parameters
	 *            The parameters in key-value pairs, in the order {@code key1},
	 *            {@code value1}, {@code key2}, {@code value2}, …
	 * @throws IllegalArgumentException
	 *             if an invalid number of parameters is given (i.e. the number
	 *             of parameters is odd)
	 */
	public MediaType(String type, String subtype, String... parameters) throws IllegalArgumentException {
		if ((parameters.length & 1) != 0) {
			throw new IllegalArgumentException("Invalid number of parameters given!");
		}
		this.type = type;
		this.subtype = subtype;
		for (int index = 0; index < parameters.length; index += 2) {
			this.parameters.put(parameters[index], parameters[index + 1]);
		}
	}

	/**
	 * Creates a new media type.
	 *
	 * @param type
	 *            The top-level type
	 * @param subtype
	 *            The subtype
	 * @param parameters
	 *            The parameters of the media type
	 */
	public MediaType(String type, String subtype, Map<String, String> parameters) {
		this.type = type;
		this.subtype = subtype;
		this.parameters.putAll(parameters);
	}

	//
	// ACCESSORS
	//

	/**
	 * Returns the top-level type of this media type.
	 *
	 * @return The top-level type
	 */
	public String getType() {
		return type;
	}

	/**
	 * Creates a new media type that has the same subtype and parameters as this
	 * media type and the given type as top-level type.
	 *
	 * @param type
	 *            The top-level type of the new media type
	 * @return The new media type
	 */
	public MediaType setType(String type) {
		return new MediaType(type, subtype, parameters);
	}

	/**
	 * Returns the subtype of this media type.
	 *
	 * @return The subtype
	 */
	public String getSubtype() {
		return subtype;
	}

	/**
	 * Creates a new media type that has the same top-level type and parameters
	 * as this media type and the given subtype as subtype.
	 *
	 * @param subtype
	 *            The subtype of the new media type
	 * @return The new media type
	 */
	public MediaType setSubtype(String subtype) {
		return new MediaType(type, subtype, parameters);
	}

	/**
	 * Returns the value of the parameter with the given name.
	 *
	 * @param name
	 *            The name of the parameter
	 * @return The value of the parameter (or {@code null} if the media type
	 *         does not have a parameter with the given name)
	 */
	public String getParameter(String name) {
		return parameters.get(name.toLowerCase());
	}

	/**
	 * Creates a new media type that has the same top-level type, subtype, and
	 * parameters as this media type but has the parameter with the given name
	 * changed to the given value.
	 *
	 * @param name
	 *            The name of the parameter to change
	 * @param value
	 *            The new value of the parameter. Null = delete parameter.
	 * @return The new media type
	 */
	public MediaType setParameter(String name, String value) {
		MediaType newMediaType = new MediaType(type, subtype, parameters);
		if(value == null)
			newMediaType.parameters.remove(name.toLowerCase());
		else
			newMediaType.parameters.put(name.toLowerCase(), value);
		return newMediaType;
	}

	/**
	 * Creates a new media type that has the same top-level type, subtype, and
	 * parameters as this media type but has the parameter with the given name
	 * removed.
	 *
	 * @param name
	 *            The name of the parameter to remove
	 * @return The new media type
	 */
	public MediaType removeParameter(String name) {
		if (!parameters.containsKey(name.toLowerCase())) {
			return this;
		}
		MediaType newMediaType = new MediaType(type, subtype, parameters);
		newMediaType.parameters.remove(name.toLowerCase());
		return newMediaType;
	}

	//
	// OBJECT METHODS
	//

	/**
	 * {@inheritDoc}
	 */
	@Override
	public String toString() {
		StringBuilder mediaType = new StringBuilder();
		mediaType.append(type).append('/').append(subtype);
		for (Entry<String, String> parameter : parameters.entrySet()) {
			if (parameter.getValue() == null) {
				continue;
			}
			mediaType.append("; ").append(parameter.getKey()).append("=\"").append(parameter.getValue()).append("\"");
		}
		return mediaType.toString();
	}

	public static String getCharsetRobust(String expectedMimeType) {
		try {
			if(expectedMimeType == null) return null;
			MediaType type = new MediaType(expectedMimeType);
			return type.getParameter("charset");
		} catch (MalformedURLException e) {
			return null;
		} catch (Throwable t) {
			// Could be malicious, hence "Robust".
			return null;
		}
	}
	
	public static String getCharsetRobustOrUTF(String expectedMimeType) {
		String charset = getCharsetRobust(expectedMimeType);
		if(charset == null) return "UTF-8";
		return charset;
	}

    public LinkedHashMap<String, String> getParameters() {
        LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
        map.putAll(parameters);
        return map;
    }

    /** Get the base type without any parameters */
    public String getPlainType() {
        return type + '/' + subtype;
    }

}
