/*******************************************************************************
 * 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.olingo.odata2.core.commons;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Pattern;

/**
 * Internally used {@link ContentType} for OData library.
 * 
 * For more details on format and content of a {@link ContentType} see
 * <code>Media Type</code> format as defined in <code>RFC 2616 chapter 3.7
 * (http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html)</code>.
 * <pre>
 * <code>
 * media-type = type "/" subtype *( ";" parameter )
 * type = token
 * subtype = token
 * </code>
 * </pre>
 * 
 * Especially for <code>Accept</code> Header as defined in
 * <code>RFC 2616 chapter 14.1 (http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html)</code>:
 * <pre>
 * <code>
 * Accept = "Accept" ":"
 * #( media-range [ accept-params ] )
 * media-range = ( "* /*"
 * | ( type "/" "*" )
 * | ( type "/" subtype )
 * ) *( ";" parameter )
 * accept-params = ";" "q" "=" qvalue *( accept-extension )
 * accept-extension = ";" token [ "=" ( token | quoted-string ) ]
 * </code>
 * </pre>
 * 
 * Especially for <code>Content-Type</code> Header as defined in
 * <code>RFC 2616 chapter 14.7 (http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html)</code>:
 * <pre>
 * <code>
 * Content-Type = "Content-Type" ":" media-type
 * </code>
 * </pre>
 * 
 * Once created a {@link ContentType} is <b>IMMUTABLE</b>.
 * 
 * 
 */
public class ContentType {

  public enum ODataFormat {
    ATOM, XML, JSON, MIME, CUSTOM
  }

  private static final Set<String> KNOWN_MIME_TYPES = new HashSet<String>();
  static {
    KNOWN_MIME_TYPES.add("audio");
    KNOWN_MIME_TYPES.add("image");
    KNOWN_MIME_TYPES.add("video");
    KNOWN_MIME_TYPES.add("multipart");
    KNOWN_MIME_TYPES.add("text");
  }

  private static final Comparator<String> Q_PARAMETER_COMPARATOR = new Comparator<String>() {
    @Override
    public int compare(final String o1, final String o2) {
      Float f1 = parseQParameterValue(o1);
      Float f2 = parseQParameterValue(o2);
      return f2.compareTo(f1);
    }
  };

  private static final char WHITESPACE_CHAR = ' ';
  private static final String PARAMETER_SEPARATOR = ";";
  private static final String PARAMETER_KEY_VALUE_SEPARATOR = "=";
  private static final String TYPE_SUBTYPE_SEPARATOR = "/";
  private static final String MEDIA_TYPE_WILDCARD = "*";
  private static final String VERBOSE = "verbose";

  public static final String PARAMETER_CHARSET = "charset";
  public static final String PARAMETER_ODATA = "odata";
  public static final String PARAMETER_Q = "q";
  public static final String PARAMETER_TYPE = "type";
  public static final String CHARSET_UTF_8 = "utf-8";

  private static final Pattern Q_PARAMETER_VALUE_PATTERN = Pattern.compile("1|0|1\\.0{1,3}|0\\.\\d{1,3}");

  public static final ContentType WILDCARD = new ContentType(MEDIA_TYPE_WILDCARD, MEDIA_TYPE_WILDCARD);

  public static final ContentType APPLICATION_XML = new ContentType("application", "xml", ODataFormat.XML);
  public static final ContentType APPLICATION_XML_CS_UTF_8 = ContentType.create(APPLICATION_XML, PARAMETER_CHARSET,
      CHARSET_UTF_8);
  public static final ContentType APPLICATION_ATOM_XML = new ContentType("application", "atom+xml", ODataFormat.ATOM);
  public static final ContentType APPLICATION_ATOM_XML_CS_UTF_8 = ContentType.create(APPLICATION_ATOM_XML,
      PARAMETER_CHARSET, CHARSET_UTF_8);
  public static final ContentType APPLICATION_ATOM_XML_ENTRY = new ContentType("application", "atom+xml",
      ODataFormat.ATOM, parameterMap(PARAMETER_TYPE, "entry"));
  public static final ContentType APPLICATION_ATOM_XML_ENTRY_CS_UTF_8 = ContentType.create(APPLICATION_ATOM_XML_ENTRY,
      PARAMETER_CHARSET, CHARSET_UTF_8);
  public static final ContentType APPLICATION_ATOM_XML_FEED = new ContentType("application", "atom+xml",
      ODataFormat.ATOM, parameterMap(PARAMETER_TYPE, "feed"));
  public static final ContentType APPLICATION_ATOM_XML_FEED_CS_UTF_8 = ContentType.create(APPLICATION_ATOM_XML_FEED,
      PARAMETER_CHARSET, CHARSET_UTF_8);
  public static final ContentType APPLICATION_ATOM_SVC =
      new ContentType("application", "atomsvc+xml", ODataFormat.ATOM);
  public static final ContentType APPLICATION_ATOM_SVC_CS_UTF_8 = ContentType.create(APPLICATION_ATOM_SVC,
      PARAMETER_CHARSET, CHARSET_UTF_8);
  public static final ContentType APPLICATION_JSON = new ContentType("application", "json", ODataFormat.JSON);
  public static final ContentType APPLICATION_JSON_ODATA_VERBOSE = ContentType.create(APPLICATION_JSON,
      PARAMETER_ODATA, VERBOSE);
  public static final ContentType APPLICATION_JSON_CS_UTF_8 = ContentType.create(APPLICATION_JSON, PARAMETER_CHARSET,
      CHARSET_UTF_8);
  public static final ContentType APPLICATION_OCTET_STREAM = new ContentType("application", "octet-stream");
  public static final ContentType TEXT_PLAIN = new ContentType("text", "plain");
  public static final ContentType TEXT_PLAIN_CS_UTF_8 = ContentType
      .create(TEXT_PLAIN, PARAMETER_CHARSET, CHARSET_UTF_8);
  public static final ContentType MULTIPART_MIXED = new ContentType("multipart", "mixed");

  private final String type;
  private final String subtype;
  private final Map<String, String> parameters;
  private final ODataFormat odataFormat;

  private ContentType(final String type) {
    if (type == null) {
      throw new IllegalArgumentException("Type parameter MUST NOT be null.");
    }
    odataFormat = ODataFormat.CUSTOM;
    this.type = validateType(type);
    subtype = null;
    parameters = Collections.emptyMap();
  }

  private ContentType(final String type, final String subtype) {
    this(type, subtype, ODataFormat.CUSTOM, null);
  }

  private ContentType(final String type, final String subtype, final ODataFormat odataFormat) {
    this(type, subtype, odataFormat, null);
  }

  private ContentType(final String type, final String subtype, final ODataFormat odataFormat,
      final Map<String, String> parameters) {
    if ((type == null || MEDIA_TYPE_WILDCARD.equals(type)) && !MEDIA_TYPE_WILDCARD.equals(subtype)) {
      throw new IllegalArgumentException("Illegal combination of WILDCARD type with NONE WILDCARD subtype.");
    }
    this.odataFormat = odataFormat;
    this.type = validateType(type);
    this.subtype = validateType(subtype);

    if (parameters == null) {
      this.parameters = Collections.emptyMap();
    } else {
      this.parameters = new TreeMap<String, String>(new Comparator<String>() {
        @Override
        public int compare(final String o1, final String o2) {
          return o1.compareToIgnoreCase(o2);
        }
      });
      this.parameters.putAll(parameters);
      this.parameters.remove(PARAMETER_Q);
    }
  }

  private String validateType(final String type) {
    if (type == null || type.isEmpty()) {
      return MEDIA_TYPE_WILDCARD;
    }
    int len = type.length();
    for (int i = 0; i < len; i++) {
      if (type.charAt(i) == WHITESPACE_CHAR) {
        throw new IllegalArgumentException("Illegal whitespace found for type '" + type + "'.");
      }
    }
    return type;
  }

  /**
   * Validates if given <code>format</code> is parseable and can be used as input for {@link #create(String)} method.
   * @param format to be validated string
   * @return <code>true</code> if format is parseable otherwise <code>false</code>
   */
  public static boolean isParseable(final String format) {
    try {
      return ContentType.create(format) != null;
    } catch (IllegalArgumentException e) {
      return false;
    }
  }

  /**
   * Validates if given <code>format</code> is parseable and can be used as input for {@link #create(String)} method.
   * @param format to be validated string
   * @return <code>true</code> if format is parseable otherwise <code>false</code>
   */
  public static boolean isParseableAsCustom(final String format) {
    try {
      return ContentType.create(format) != null;
    } catch (IllegalArgumentException e) {
      return false;
    }
  }

  /**
   * Creates a content type from type and subtype
   * @param type
   * @param subtype
   * @return a new <code>ContentType</code> object
   */
  public static ContentType create(final String type, final String subtype) {
    return new ContentType(type, subtype, mapToODataFormat(type, subtype), null);
  }

  /**
   * 
   * @param type
   * @param subtype
   * @param parameters
   * @return a new <code>ContentType</code> object
   */
  public static ContentType create(final String type, final String subtype, final Map<String, String> parameters) {
    return new ContentType(type, subtype, mapToODataFormat(type, subtype), parameters);
  }

  /**
   * 
   * @param contentType
   * @param parameterKey
   * @param parameterValue
   * @return a new <code>ContentType</code> object
   */
  public static ContentType
      create(final ContentType contentType, final String parameterKey, final String parameterValue) {
    ContentType ct =
        new ContentType(contentType.type, contentType.subtype, contentType.odataFormat, contentType.parameters);
    ct.parameters.put(parameterKey, parameterValue);
    return ct;
  }

  /**
   * Create a {@link ContentType} based on given input string (<code>format</code>).
   * 
   * Supported format is <code>Media Type</code> format as defined in <code>RFC 2616 chapter 3.7</code>.
   * This format is used as
   * <code>HTTP Accept HEADER</code> format as defined in <code>RFC 2616 chapter 14.1</code>
   * and
   * <code>HTTP Content-Type HEADER</code> format as defined in <code>RFC 2616 chapter 14.17</code>
   * 
   * @param format a string in format as defined in <code>RFC 2616 section 3.7</code>
   * @return a new <code>ContentType</code> object
   * @throws IllegalArgumentException if input string is not parseable
   */
  public static ContentType create(final String format) {
    if (format == null) {
      throw new IllegalArgumentException("Parameter format MUST NOT be NULL.");
    }

    // split 'types' and 'parameters'
    String[] typesAndParameters = format.split(PARAMETER_SEPARATOR, 2);
    String types = typesAndParameters[0];
    String parameters = (typesAndParameters.length > 1 ? typesAndParameters[1] : null);
    //
    Map<String, String> parametersMap = parseParameters(parameters);
    //
    if (types.contains(TYPE_SUBTYPE_SEPARATOR)) {
      String[] tokens = types.split(TYPE_SUBTYPE_SEPARATOR);
      if (tokens.length == 2) {
        if (tokens[0] == null || tokens[0].isEmpty()) {
          throw new IllegalArgumentException("No type found in format '" + format + "'.");
        } else if (tokens[1] == null || tokens[1].isEmpty()) {
          throw new IllegalArgumentException("No subtype found in format '" + format + "'.");
        } else {
          return create(tokens[0], tokens[1], parametersMap);
        }
      } else {
        throw new IllegalArgumentException("Too many '" + TYPE_SUBTYPE_SEPARATOR + "' in format '" + format + "'.");
      }
    } else if (MEDIA_TYPE_WILDCARD.equals(types)) {
      return ContentType.WILDCARD;
    } else {
      throw new IllegalArgumentException("No separator '" + TYPE_SUBTYPE_SEPARATOR + "' was found in format '" + format
          + "'.");
    }
  }

  /**
   * Create a {@link ContentType} based on given input string (<code>format</code>).
   * 
   * Supported format is <code>Media Type</code> format as defined in <code>RFC 2616 chapter 3.7</code>.
   * and {@link ContentType} with {@link ODataFormat#CUSTOM}.
   * 
   * The <code>Media Type</code> format can be used as
   * <code>HTTP Accept HEADER</code> format as defined in <code>RFC 2616 chapter 14.1</code>
   * and
   * <code>HTTP Content-Type HEADER</code> format as defined in <code>RFC 2616 chapter 14.17</code>.
   * The {@link ContentType} with {@link ODataFormat#CUSTOM} can only be used as <code>$format</code> system query
   * option
   * (as defined
   * http://www.odata.org/documentation/odata-v2-documentation/uri-conventions/#47_Format_System_Query_Option_format).
   * 
   * @param format a string in format as defined in <code>RFC 2616 section 3.7</code>
   * @return a new <code>ContentType</code> object
   * @throws IllegalArgumentException if input string is not parseable
   */
  public static ContentType createAsCustom(final String format) {
    ContentType parsedContentType = parse(format);
    if (parsedContentType == null) {
      return new ContentType(format);
    }
    return parsedContentType;
  }

  /**
   * Create a list of {@link ContentType} based on given input strings (<code>contentTypes</code>).
   * 
   * Supported format is <code>Media Type</code> format as defined in <code>RFC 2616 chapter 3.7</code>.
   * This format is used as
   * <code>HTTP Accept HEADER</code> format as defined in <code>RFC 2616 chapter 14.1</code>
   * and
   * <code>HTTP Content-Type HEADER</code> format as defined in <code>RFC 2616 chapter 14.17</code>.
   * <p>
   * If one of the given strings can not be parsed an exception is thrown (hence no list is returned with the parseable
   * strings).
   * </p>
   * 
   * @param contentTypeStrings a list of strings in format as defined in <code>RFC 2616 section 3.7</code>
   * @return a list of new <code>ContentType</code> object
   * @throws IllegalArgumentException if one of the given input string is not parseable this exceptions is thrown
   */
  public static List<ContentType> create(final List<String> contentTypeStrings) {
    List<ContentType> contentTypes = new ArrayList<ContentType>(contentTypeStrings.size());
    for (String contentTypeString : contentTypeStrings) {
      contentTypes.add(create(contentTypeString));
    }
    return contentTypes;
  }

  /**
   * Create a list of {@link ContentType} based on given input strings (<code>contentTypes</code>).
   * 
   * Supported format is <code>Media Type</code> format as defined in <code>RFC 2616 chapter 3.7</code>.
   * and {@link ContentType} with {@link ODataFormat#CUSTOM}.
   * 
   * The <code>Media Type</code> format can be used as
   * <code>HTTP Accept HEADER</code> format as defined in <code>RFC 2616 chapter 14.1</code>
   * and
   * <code>HTTP Content-Type HEADER</code> format as defined in <code>RFC 2616 chapter 14.17</code>.
   * The {@link ContentType} with {@link ODataFormat#CUSTOM} can only be used as <code>$format</code> system query
   * option
   * (as defined
   * http://www.odata.org/documentation/odata-v2-documentation/uri-conventions/#47_Format_System_Query_Option_format).
   * 
   * @param contentTypeStrings a list of strings in format as defined in <code>RFC 2616 section 3.7</code> or
   * as defined
   * http://www.odata.org/documentation/odata-v2-documentation/uri-conventions/#47_Format_System_Query_Option_format
   * @return a list of new <code>ContentType</code> object
   * @throws IllegalArgumentException if one of the given input string is not parseable this exceptions is thrown
   */
  public static List<ContentType> createAsCustom(final List<String> contentTypeStrings) {
    List<ContentType> contentTypes = new ArrayList<ContentType>(contentTypeStrings.size());
    for (String contentTypeString : contentTypeStrings) {
      contentTypes.add(createAsCustom(contentTypeString));
    }
    return contentTypes;
  }

  /**
   * Parses the given input string (<code>format</code>) and returns created {@link ContentType} if input was valid or
   * return <code>NULL</code> if
   * input was not parseable.
   * 
   * For the definition of the supported format see {@link #create(String)}.
   * 
   * @param format a string in format as defined in <code>RFC 2616 section 3.7</code>
   * @return a new <code>ContentType</code> object
   */
  public static ContentType parse(final String format) {
    try {
      return ContentType.create(format);
    } catch (IllegalArgumentException e) {
      return null;
    }
  }

  /**
   * Sort given list (which must contains content type formated string) for their {@value #PARAMETER_Q} value
   * as defined in <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1">RFC 2616 section 4.1</a> and
   * <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.9">RFC 2616 Section 3.9</a>.
   * 
   * <b>Attention:</b> For invalid values a {@value #PARAMETER_Q} value from <code>-1</code> is used for sorting.
   * 
   * @param toSort list which is sorted and hence re-arranged
   */
  public static void sortForQParameter(final List<String> toSort) {
    Collections.sort(toSort, ContentType.Q_PARAMETER_COMPARATOR);
  }

  /**
   * Map combination of type/subtype to corresponding {@link ODataFormat}.
   * 
   * @param type type which is used for mapping
   * @param subtype subtype which is used for mapping
   * @return corresponding {@link ODataFormat} or {@link ODataFormat#CUSTOM} if none specific format is available.
   */
  private static ODataFormat mapToODataFormat(final String type, final String subtype) {
    ODataFormat odataFormat = ODataFormat.CUSTOM;
    if (type.contains("application")) {
      if (subtype.contains("atom")) {
        odataFormat = ODataFormat.ATOM;
      } else if (subtype.contains("xml")) {
        odataFormat = ODataFormat.XML;
      } else if (subtype.contains("json")) {
        odataFormat = ODataFormat.JSON;
      }
    } else if (KNOWN_MIME_TYPES.contains(type)) {
      odataFormat = ODataFormat.MIME;
    }
    return odataFormat;
  }

  /**
   * Maps content of array into map.
   * Therefore it must be an combination of <code>key</code> followed by the <code>value</code> in the array.
   * 
   * @param content content which is added to {@link Map}.
   * @return a new <code>ContentType</code> object
   */
  private static Map<String, String> parameterMap(final String... content) {
    Map<String, String> map = new HashMap<String, String>();
    for (int i = 0; i < content.length - 1; i += 2) {
      String key = content[i];
      String value = content[i + 1];
      map.put(key, value);
    }
    return map;
  }

  /**
   * Valid input are <code>;</code> separated <code>key=value</code> pairs
   * without spaces between key and value.
   * <b>Attention:</b> <code>q</code> parameter is validated but not added to result map
   * 
   * <p>
   * See RFC 2616:
   * The type, subtype, and parameter attribute names are case-insensitive.
   * Parameter values might or might not be case-sensitive, depending on the
   * semantics of the parameter name. <b>Linear white space (LWS) MUST NOT be used
   * between the type and subtype, nor between an attribute and its value</b>.
   * </p>
   * 
   * @param parameters
   * @return Map with keys mapped to values
   */
  private static Map<String, String> parseParameters(final String parameters) {
    Map<String, String> parameterMap = new HashMap<String, String>();
    if (parameters != null) {
      String[] splittedParameters = parameters.split(PARAMETER_SEPARATOR);
      for (String parameter : splittedParameters) {
        String[] keyValue = parameter.split(PARAMETER_KEY_VALUE_SEPARATOR);
        String key = keyValue[0].trim().toLowerCase(Locale.ENGLISH);
        String value = keyValue.length > 1 ? keyValue[1] : null;
        if (value != null && isLws(value.charAt(0))) {
          throw new IllegalArgumentException("Value of parameter '" + key + "' starts with a LWS ('" + parameters
              + "').");
        }
        if (PARAMETER_Q.equals(key.toLowerCase(Locale.US))) {
          // q parameter is only validated but not added
          if (!Q_PARAMETER_VALUE_PATTERN.matcher(value).matches()) {
            throw new IllegalArgumentException("Value of 'q' parameter is not valid (q='" + value + "').");
          }
        } else {
          parameterMap.put(key, value);
        }
      }
    }
    return parameterMap;
  }

  /**
   * Parse value of {@value #PARAMETER_Q} <code>parameter</code> out of content type/parameters.
   * If no {@value #PARAMETER_Q} <code>parameter</code> is in <code>content type/parameters</code> parameter found
   * <code>1</code> is returned.
   * If {@value #PARAMETER_Q} <code>parameter</code> is invalid <code>-1</code> is returned.
   * 
   * @param contentType parameter which is parsed for {@value #PARAMETER_Q} <code>parameter</code> value
   * @return value of {@value #PARAMETER_Q} <code>parameter</code> or <code>1</code> or <code>-1</code>
   */
  private static Float parseQParameterValue(final String contentType) {
    if (contentType != null) {
      String[] splittedParameters = contentType.split(PARAMETER_SEPARATOR);
      for (String parameter : splittedParameters) {
        String[] keyValue = parameter.split(PARAMETER_KEY_VALUE_SEPARATOR);
        String key = keyValue[0].trim().toLowerCase(Locale.ENGLISH);
        if (PARAMETER_Q.equalsIgnoreCase(key)) {
          String value = keyValue.length > 1 ? keyValue[1] : null;
          if (Q_PARAMETER_VALUE_PATTERN.matcher(value).matches()) {
            return Float.valueOf(value);
          }
          return Float.valueOf(-1);
        }
      }
    }
    return Float.valueOf(1);
  }

  /**
   * Check if parameter with key value is a allowed parameter.
   * 
   * @param key
   * @return
   */
  private static boolean isParameterAllowed(final String key) {
    return key != null && !PARAMETER_Q.equals(key.toLowerCase(Locale.US));
  }

  /**
   * Validate if given character is a linear whitepace (includes <code>horizontal-tab, linefeed, carriage return and
   * space</code>).
   * 
   * @param character to be checked
   * @return <code>true</code> if character is a LWS, otherwise <code>false</code>.
   */
  private static boolean isLws(final char character) {
    switch (character) {
    case 9: // HT = <US-ASCII HT, horizontal-tab (9)>
    case 10: // LF = <US-ASCII LF, linefeed (10)>
    case 13: // CR = <US-ASCII CR, carriage return (13)>
    case 32: // SP = <US-ASCII SP, space (32)>
      return true;
    default:
      return false;
    }
  }

  /**
   * Ensure that charset parameter ({@link #PARAMETER_CHARSET}) is set on returned content type
   * if this {@link ContentType} is a <code>odata text related</code> content type (@see
   * {@link #isContentTypeODataTextRelated()}).
   * If <code>this</code> {@link ContentType} has no charset parameter set a new {@link ContentType} with given
   * <code>defaultCharset</code> is created.
   * Otherwise if charset parameter is already set nothing is done.
   * 
   * @param defaultCharset
   * @return ContentType
   */
  public ContentType receiveWithCharsetParameter(final String defaultCharset) {
    if (isContentTypeODataTextRelated()) {
      if (!parameters.containsKey(ContentType.PARAMETER_CHARSET)) {
        return ContentType.create(this, ContentType.PARAMETER_CHARSET, defaultCharset);
      }
    }
    return this;
  }

  /**
   * 
   * @return <code>true</code> if this {@link ContentType} is text related (in the view of OData)
   */
  public boolean isContentTypeODataTextRelated() {
    return (ContentType.TEXT_PLAIN.equals(this)
        || (getODataFormat() == ODataFormat.XML)
        || (getODataFormat() == ODataFormat.ATOM)
        || (getODataFormat() == ODataFormat.JSON));
  }

  public String getType() {
    return type;
  }

  public String getSubtype() {
    return subtype;
  }

  /**
   * 
   * @return parameters of this {@link ContentType} as unmodifiable map.
   */
  public Map<String, String> getParameters() {
    return Collections.unmodifiableMap(parameters);
  }

  @Override
  public int hashCode() {
    return 1;
  }

  /**
   * {@link ContentType}s are equal
   * <ul>
   * <li>if <code>type</code>, <code>subtype</code> and all <code>parameters</code> have the same value.</li>
   * <li>if <code>type</code> and/or <code>subtype</code> is set to "*" (in such a case the <code>parameters</code> are
   * ignored).</li>
   * </ul>
   * 
   * @return <code>true</code> if both instances are equal (see definition above), otherwise <code>false</code>.
   */
  @Override
  public boolean equals(final Object obj) {
    // NULL validation is done in method 'isEqualWithoutParameters(obj)'
    Boolean compatible = isEqualWithoutParameters(obj);

    if (compatible == null) {
      ContentType other = (ContentType) obj;

      // parameter checks
      if (parameters == null) {
        if (other.parameters != null) {
          return false;
        }
      } else if (parameters.size() == other.parameters.size()) {
        Iterator<Entry<String, String>> entries = parameters.entrySet().iterator();
        Iterator<Entry<String, String>> otherEntries = other.parameters.entrySet().iterator();
        while (entries.hasNext()) {
          Entry<String, String> e = entries.next();
          Entry<String, String> oe = otherEntries.next();

          if (!areEqual(e.getKey(), oe.getKey())) {
            return false;
          }
          if (!areEqual(e.getValue(), oe.getValue())) {
            return false;
          }
        }
      } else {
        return false;
      }
      return true;
    } else {
      // all tests run
      return compatible.booleanValue();
    }
  }

  /**
   * {@link ContentType}s are <b>compatible</b>
   * <ul>
   * <li>if <code>type</code>, <code>subtype</code> have the same value.</li>
   * <li>if <code>type</code> and/or <code>subtype</code> is set to "*"</li>
   * </ul>
   * The set <code>parameters</code> are <b>always</b> ignored (for compare with parameters see {@link #equals(Object)}
   * ).
   * 
   * @return <code>true</code> if both instances are equal (see definition above), otherwise <code>false</code>.
   */
  public boolean isCompatible(final ContentType obj) {
    Boolean compatible = isEqualWithoutParameters(obj);
    if (compatible == null) {
      return true;
    }
    return compatible.booleanValue();
  }

  /**
   * Check equal without parameters.
   * It is possible that no decision about <code>equal/none equal</code> can be determined a <code>NULL</code> is
   * returned.
   * 
   * @param obj to checked object
   * @return <code>true</code> if both instances are equal (see definition above), otherwise <code>false</code>
   * or <code>NULL</code> if no decision about <code>equal/none equal</code> could be determined.
   */
  private Boolean isEqualWithoutParameters(final Object obj) {
    // basic checks
    if (this == obj) {
      return true;
    }
    if (obj == null) {
      return false;
    }
    if (getClass() != obj.getClass()) {
      return false;
    }

    ContentType other = (ContentType) obj;

    // subtype checks
    if (subtype == null) {
      if (other.subtype != null) {
        return false;
      }
    } else if (!subtype.equals(other.subtype)) {
      if (other.subtype == null) {
        return false;
      } else if (!subtype.equals(MEDIA_TYPE_WILDCARD) && !other.subtype.equals(MEDIA_TYPE_WILDCARD)) {
        return false;
      }
    }

    // type checks
    if (type == null) {
      if (other.type != null) {
        return false;
      }
    } else if (!type.equals(other.type)) {
      if (!type.equals(MEDIA_TYPE_WILDCARD) && !other.type.equals(MEDIA_TYPE_WILDCARD)) {
        return false;
      }
    }

    // if wildcards are set, content types are defined as 'equal'
    if (countWildcards() > 0 || other.countWildcards() > 0) {
      return true;
    }

    return null;
  }

  /**
   * Check whether both string are equal ignoring the case of the strings.
   * 
   * @param first first string
   * @param second second string
   * @return <code>true</code> if both strings are equal (by ignoring the case), otherwise <code>false</code> is
   * returned
   */
  private static boolean areEqual(final String first, final String second) {
    if (first == null) {
      if (second != null) {
        return false;
      }
    } else if (!first.equalsIgnoreCase(second)) {
      return false;
    }
    return true;
  }

  /**
   * Get {@link ContentType} as string as defined in RFC 2616 (http://www.ietf.org/rfc/rfc2616.txt - chapter 14.17:
   * Content-Type)
   * 
   * @return string representation of <code>ContentType</code> object
   */
  public String toContentTypeString() {
    StringBuilder sb = new StringBuilder();

    if (odataFormat == ODataFormat.CUSTOM && subtype == null) {
      sb.append(type);
    } else {
      sb.append(type).append(TYPE_SUBTYPE_SEPARATOR).append(subtype);
    }
    
    for (Entry<String, String> parameter : parameters.entrySet()) {
      if (isParameterAllowed(parameter.getKey())) {
        String value = parameter.getValue();
        sb.append(";").append(parameter.getKey()).append("=").append(value);
      }
    }
    
    return sb.toString();
  }

  @Override
  public String toString() {
    return toContentTypeString();
  }

  public ODataFormat getODataFormat() {
    return odataFormat;
  }

  /**
   * Find best match between this {@link ContentType} and the {@link ContentType} in the list.
   * If a match (this {@link ContentType} is equal to a {@link ContentType} in list) is found either this or the
   * {@link ContentType} from the list is returned based on which {@link ContentType} has less "**" characters set
   * (checked with {@link #compareWildcardCounts(ContentType)}.
   * If no match (none {@link ContentType} in list is equal to this {@link ContentType}) is found <code>NULL</code> is
   * returned.
   * 
   * @param toMatchContentTypes list of {@link ContentType}s which are matches against this {@link ContentType}
   * @return best matched content type in list or <code>NULL</code> if none content type match to this content type
   * instance
   */
  public ContentType match(final List<ContentType> toMatchContentTypes) {
    for (ContentType supportedContentType : toMatchContentTypes) {
      if (equals(supportedContentType)) {
        if (compareWildcardCounts(supportedContentType) < 0) {
          return this;
        } else {
          return supportedContentType;
        }
      }
    }
    return null;
  }

  /**
   * Find best match between this {@link ContentType} and the {@link ContentType} in the list ignoring all set
   * parameters.
   * If a match (this {@link ContentType} is equal to a {@link ContentType} in list) is found either this or the
   * {@link ContentType} from the list is returned based on which {@link ContentType} has less "**" characters set
   * (checked with {@link #compareWildcardCounts(ContentType)}.
   * If no match (none {@link ContentType} in list is equal to this {@link ContentType}) is found <code>NULL</code> is
   * returned.
   * 
   * @param toMatchContentTypes list of {@link ContentType}s which are matches against this {@link ContentType}
   * @return best matched content type in list or <code>NULL</code> if none content type match to this content type
   * instance
   */
  public ContentType matchCompatible(final List<ContentType> toMatchContentTypes) {
    for (ContentType supportedContentType : toMatchContentTypes) {
      if (isCompatible(supportedContentType)) {
        if (compareWildcardCounts(supportedContentType) < 0) {
          return this;
        } else {
          return supportedContentType;
        }
      }
    }
    return null;
  }

  /**
   * Check if a valid compatible match for this {@link ContentType} exists in given list.
   * Compatible in this case means that <b>all set parameters are ignored</b>.
   * For more detail what a valid match is see {@link #matchCompatible(List)}.
   * 
   * @param toMatchContentTypes list of {@link ContentType}s which are matches against this {@link ContentType}
   * @return <code>true</code> if a compatible content type was found in given list
   * or <code>false</code> if none compatible content type match was found
   */
  public boolean hasCompatible(final List<ContentType> toMatchContentTypes) {
    return matchCompatible(toMatchContentTypes) != null;
  }

  /**
   * Check if a valid match for this {@link ContentType} exists in given list.
   * For more detail what a valid match is see {@link #match(List)}.
   * 
   * @param toMatchContentTypes list of {@link ContentType}s which are matches against this {@link ContentType}
   * @return <code>true</code> if a matching content type was found in given list
   * or <code>false</code> if none matching content type match was found
   */
  public boolean hasMatch(final List<ContentType> toMatchContentTypes) {
    return match(toMatchContentTypes) != null;
  }

  /**
   * Compare wildcards counts/weights of both {@link ContentType}.
   * 
   * The smaller {@link ContentType} has lesser weighted wildcards then the bigger {@link ContentType}.
   * As result this method returns this object weighted wildcards minus the given parameter object weighted wildcards.
   * 
   * A type wildcard is weighted with <code>2</code> and a subtype wildcard is weighted with <code>1</code>.
   * 
   * @param otherContentType {@link ContentType} to be compared to
   * @return this object weighted wildcards minus the given parameter object weighted wildcards.
   */
  public int compareWildcardCounts(final ContentType otherContentType) {
    return countWildcards() - otherContentType.countWildcards();
  }

  private int countWildcards() {
    int count = 0;
    if (MEDIA_TYPE_WILDCARD.equals(type)) {
      count += 2;
    }
    if (MEDIA_TYPE_WILDCARD.equals(subtype)) {
      count++;
    }
    return count;
  }

  /**
   * 
   * @return <code>true</code> if <code>type</code> or <code>subtype</code> of this instance is a "*".
   */
  public boolean hasWildcard() {
    return (MEDIA_TYPE_WILDCARD.equals(type) || MEDIA_TYPE_WILDCARD.equals(subtype));
  }

  /**
   * 
   * @return <code>true</code> if both <code>type</code> and <code>subtype</code> of this instance are a "*".
   */
  public boolean isWildcard() {
    return (MEDIA_TYPE_WILDCARD.equals(type) && MEDIA_TYPE_WILDCARD.equals(subtype));
  }

  public static List<ContentType> convert(final List<String> types) {
    List<ContentType> results = new ArrayList<ContentType>();
    for (String contentType : types) {
      results.add(ContentType.create(contentType));
    }
    return results;
  }

  /**
   * Check if a valid match for given content type formated string (<code>toMatch</code>) exists in given list.
   * Therefore the given content type formated string (<code>toMatch</code>) is converted into a {@link ContentType}
   * with a simple {@link #create(String)} call (during which an exception can occur).
   * 
   * For more detail in general see {@link #hasMatch(List)} and for what a valid match is see {@link #match(List)}.
   * 
   * @param toMatch content type formated string (<code>toMatch</code>) for which is checked if a match exists in given
   * list
   * @param matchExamples list of {@link ContentType}s which are matches against content type formated string
   * (<code>toMatch</code>)
   * @return <code>true</code> if a matching content type was found in given list
   * or <code>false</code> if none matching content type match was found
   */
  public static boolean match(final String toMatch, final ContentType... matchExamples) {
    ContentType toMatchContentType = ContentType.create(toMatch);

    return toMatchContentType.hasMatch(Arrays.asList(matchExamples));
  }
}
