// ***************************************************************************************************************************
// * 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.juneau.http;

import static org.apache.juneau.internal.CollectionUtils.*;
import static org.apache.juneau.internal.StringUtils.*;

import java.util.*;
import java.util.concurrent.*;

import org.apache.juneau.annotation.*;
import org.apache.juneau.internal.*;
import org.apache.juneau.json.*;


/**
 * Describes a single media type used in content negotiation between an HTTP client and server, as described in
 * Section 14.1 and 14.7 of RFC2616 (the HTTP/1.1 specification).
 *
 * <ul class='seealso'>
 * 	<li class='extlink'>{@doc RFC2616}
 * </ul>
 */
@BeanIgnore
public class MediaType implements Comparable<MediaType> {

	private static final boolean NOCACHE = Boolean.getBoolean("juneau.nocache");
	private static final ConcurrentHashMap<String,MediaType> CACHE = new ConcurrentHashMap<>();

	/** Reusable predefined media type */
	@SuppressWarnings("javadoc")
	public static final MediaType
		CSV = forString("text/csv"),
		HTML = forString("text/html"),
		JSON = forString("application/json"),
		MSGPACK = forString("octal/msgpack"),
		PLAIN = forString("text/plain"),
		UON = forString("text/uon"),
		URLENCODING = forString("application/x-www-form-urlencoded"),
		XML = forString("text/xml"),
		XMLSOAP = forString("text/xml+soap"),

		RDF = forString("text/xml+rdf"),
		RDFABBREV = forString("text/xml+rdf+abbrev"),
		NTRIPLE = forString("text/n-triple"),
		TURTLE = forString("text/turtle"),
		N3 = forString("text/n3")
	;

	private final String mediaType;
	private final String type;								     // The media type (e.g. "text" for Accept, "utf-8" for Accept-Charset)
	private final String subType;                        // The media sub-type (e.g. "json" for Accept, not used for Accept-Charset)
	private final String[] subTypes;                     // The media sub-type (e.g. "json" for Accept, not used for Accept-Charset)
	private final String[] subTypesSorted;               // Same as subTypes, but sorted so that it can be used for comparison.
	private final List<String> subTypesList;             // The media sub-type (e.g. "json" for Accept, not used for Accept-Charset)
	private final Map<String,Set<String>> parameters;    // The media type parameters (e.g. "text/html;level=1").  Does not include q!
	private final boolean hasSubtypeMeta;                // The media subtype contains meta-character '*'.

	/**
	 * Returns the media type for the specified string.
	 * The same media type strings always return the same objects so that these objects
	 * can be compared for equality using '=='.
	 *
	 * <ul class='notes'>
	 * 	<li>
	 * 		Spaces are replaced with <js>'+'</js> characters.
	 * 		This gets around the issue where passing media type strings with <js>'+'</js> as HTTP GET parameters
	 * 		get replaced with spaces by your browser.  Since spaces aren't supported by the spec, this
	 * 		is doesn't break anything.
	 * 	<li>
	 * 		Anything including and following the <js>';'</js> character is ignored (e.g. <js>";charset=X"</js>).
	 * </ul>
	 *
	 * @param s
	 * 	The media type string.
	 * 	Will be lowercased.
	 * 	Returns <jk>null</jk> if input is null or empty.
	 * @return A cached media type object.
	 */
	public static MediaType forString(String s) {
		if (isEmpty(s))
			return null;
		MediaType mt = CACHE.get(s);
		if (mt != null)
			return mt;
		mt = new MediaType(s);
		if (NOCACHE)
			return mt;
		CACHE.putIfAbsent(s, mt);
		return CACHE.get(s);
	}

	/**
	 * Same as {@link #forString(String)} but allows you to construct an array of <c>MediaTypes</c> from an
	 * array of strings.
	 *
	 * @param s
	 * 	The media type strings.
	 * @return
	 * 	An array of <c>MediaType</c> objects.
	 * 	<br>Always the same length as the input string array.
	 */
	public static MediaType[] forStrings(String...s) {
		MediaType[] mt = new MediaType[s.length];
		for (int i = 0; i < s.length; i++)
			mt[i] = forString(s[i]);
		return mt;
	}

	MediaType(String mt) {
		Builder b = new Builder(mt);
		this.mediaType = b.mediaType;
		this.type = b.type;
		this.subType = b.subType;
		this.subTypes = b.subTypes;
		this.subTypesSorted = b.subTypesSorted;
		this.subTypesList = immutableList(subTypes);
		this.parameters = unmodifiableMap(b.parameters);
		this.hasSubtypeMeta = b.hasSubtypeMeta;
	}

	static final class Builder {
		String mediaType, type, subType;
		String[] subTypes, subTypesSorted;
		Map<String,Set<String>> parameters;
		boolean hasSubtypeMeta;

		Builder(String mt) {
			mt = mt.trim();
			int i = mt.indexOf(',');
			if (i != -1)
				mt = mt.substring(0, i);

			i = mt.indexOf(';');
			if (i == -1) {
				this.parameters = Collections.emptyMap();
			} else {
				this.parameters = new TreeMap<>();
				String[] tokens = mt.substring(i+1).split(";");

				for (int j = 0; j < tokens.length; j++) {
					String[] parm = tokens[j].split("=");
					if (parm.length == 2) {
						String k = parm[0].trim(), v = parm[1].trim();
						if (! parameters.containsKey(k))
							parameters.put(k, new TreeSet<String>());
						parameters.get(k).add(v);
					}
				}

				mt = mt.substring(0, i);
			}

			this.mediaType = mt;
			if (mt != null) {
				mt = mt.replace(' ', '+');
				i = mt.indexOf('/');
				type = (i == -1 ? mt : mt.substring(0, i));
				subType = (i == -1 ? "*" : mt.substring(i+1));
			}
			this.subTypes = StringUtils.split(subType, '+');
			this.subTypesSorted = Arrays.copyOf(subTypes, subTypes.length);
			Arrays.sort(this.subTypesSorted);
			hasSubtypeMeta = ArrayUtils.contains("*", this.subTypes);
		}
	}

	/**
	 * Returns the <js>'type'</js> fragment of the <js>'type/subType'</js> string.
	 *
	 * @return The media type.
	 */
	public final String getType() {
		return type;
	}

	/**
	 * Returns the <js>'subType'</js> fragment of the <js>'type/subType'</js> string.
	 *
	 * @return The media subtype.
	 */
	public final String getSubType() {
		return subType;
	}

	/**
	 * Returns <jk>true</jk> if the subtype contains the specified <js>'+'</js> delimited subtype value.
	 *
	 * @param st
	 * 	The subtype string.
	 * 	Case is ignored.
	 * @return <jk>true</jk> if the subtype contains the specified subtype string.
	 */
	public final boolean hasSubType(String st) {
		if (st != null)
			for (String s : subTypes)
				if (st.equalsIgnoreCase(s))
					return true;
		return false;
	}

	/**
	 * Returns the subtypes broken down by fragments delimited by <js>"'"</js>.
	 *
	 * <P>
	 * For example, the media type <js>"text/foo+bar"</js> will return a list of
	 * <code>[<js>'foo'</js>,<js>'bar'</js>]</code>
	 *
	 * @return An unmodifiable list of subtype fragments.  Never <jk>null</jk>.
	 */
	public final List<String> getSubTypes() {
		return subTypesList;
	}

	/**
	 * Returns <jk>true</jk> if this media type contains the <js>'*'</js> meta character.
	 *
	 * @return <jk>true</jk> if this media type contains the <js>'*'</js> meta character.
	 */
	public final boolean isMeta() {
		return hasSubtypeMeta;
	}

	/**
	 * Returns a match metric against the specified media type where a larger number represents a better match.
	 *
	 * <p>
	 * This media type can contain <js>'*'</js> metacharacters.
	 * <br>The comparison media type must not.
	 *
	 * <ul>
	 * 	<li>Exact matches (e.g. <js>"text/json"<js>/</js>"text/json"</js>) should match
	 * 		better than meta-character matches (e.g. <js>"text/*"<js>/</js>"text/json"</js>)
	 * 	<li>The comparison media type can have additional subtype tokens (e.g. <js>"text/json+foo"</js>)
	 * 		that will not prevent a match if the <c>allowExtraSubTypes</c> flag is set.
	 * 		The reverse is not true, e.g. the comparison media type must contain all subtype tokens found in the
	 * 		comparing media type.
	 * 		<ul>
	 * 			<li>We want the {@link JsonSerializer} (<js>"text/json"</js>) class to be able to handle requests for <js>"text/json+foo"</js>.
	 * 			<li>We want to make sure {@link org.apache.juneau.json.SimpleJsonSerializer} (<js>"text/json+simple"</js>) does not handle
	 * 				requests for <js>"text/json"</js>.
	 * 		</ul>
	 * 		More token matches should result in a higher match number.
	 * </ul>
	 *
	 * The formula is as follows for <c>type/subTypes</c>:
	 * <ul>
	 * 	<li>An exact match is <c>100,000</c>.
	 * 	<li>Add the following for type (assuming subtype match is &lt;0):
	 * 	<ul>
	 * 		<li><c>10,000</c> for an exact match (e.g. <js>"text"</js>==<js>"text"</js>).
	 * 		<li><c>5,000</c> for a meta match (e.g. <js>"*"</js>==<js>"text"</js>).
	 * 	</ul>
	 * 	<li>Add the following for subtype (assuming type match is &lt;0):
	 * 	<ul>
	 * 		<li><c>7,500</c> for an exact match (e.g. <js>"json+foo"</js>==<js>"json+foo"</js> or <js>"json+foo"</js>==<js>"foo+json"</js>)
	 * 		<li><c>100</c> for every subtype entry match (e.g. <js>"json"</js>/<js>"json+foo"</js>)
	 * 	</ul>
	 * </ul>
	 *
	 * @param o The media type to compare with.
	 * @param allowExtraSubTypes If <jk>true</jk>,
	 * @return <jk>true</jk> if the media types match.
	 */
	public final int match(MediaType o, boolean allowExtraSubTypes) {

		// Perfect match
		if (this == o || (type.equals(o.type) && subType.equals(o.subType)))
			return 100000;

		int c = 0;

		if (type.equals(o.type))
			c += 10000;
		else if ("*".equals(type) || "*".equals(o.type))
			c += 5000;

		if (c == 0)
			return 0;

		// Subtypes match but are ordered different
		if (ArrayUtils.equals(subTypesSorted, o.subTypesSorted))
			return c + 7500;

		for (String st1 : subTypes) {
			if ("*".equals(st1))
				c += 0;
			else if (ArrayUtils.contains(st1, o.subTypes))
				c += 100;
			else if (o.hasSubtypeMeta)
				c += 0;
			else
				return 0;
		}
		for (String st2 : o.subTypes) {
			if ("*".equals(st2))
				c += 0;
			else if (ArrayUtils.contains(st2, subTypes))
				c += 100;
			else if (hasSubtypeMeta)
				c += 0;
			else if (! allowExtraSubTypes)
				return 0;
			else
				c += 10;
		}

		return c;
	}

	/**
	 * Returns the additional parameters on this media type.
	 *
	 * <p>
	 * For example, given the media type string <js>"text/html;level=1"</js>, will return a map
	 * with the single entry <code>{level:[<js>'1'</js>]}</code>.
	 *
	 * @return The map of additional parameters, or an empty map if there are no parameters.
	 */
	public final Map<String,Set<String>> getParameters() {
		return parameters;
	}

	@Override /* Object */
	public final String toString() {
		if (parameters.isEmpty())
			return mediaType;
		StringBuilder sb = new StringBuilder(mediaType);
		for (Map.Entry<String,Set<String>> e : parameters.entrySet())
			for (String value : e.getValue())
				sb.append(';').append(e.getKey()).append('=').append(value);
		return sb.toString();
	}

	@Override /* Object */
	public final int hashCode() {
		return mediaType.hashCode();
	}

	@Override /* Object */
	public final boolean equals(Object o) {
		return this == o;
	}

	@Override
	public final int compareTo(MediaType o) {
		return mediaType.compareTo(o.mediaType);
	}
}
