/* 
 * E-XML Library:  For XML, XML-RPC, HTTP, and related.
 * Copyright (C) 2002-2008  Elias Ross
 * 
 * genman@noderunner.net
 * http://noderunner.net/~genman
 * 
 * 1025 NE 73RD ST
 * SEATTLE WA 98115
 * USA
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 * 
 * $Id$
 */

package net.noderunner.http;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Content type field value class, see RFC 2045 section 5.1 on this.
 * Immutable class.
 * 
 * @author Elias Ross
 */
public final class ContentType {

	/**
	 * Standard content types.
	 */
	public enum StandardType {
		application, audio, image, message, multipart, text, video;
	}
	
	private StandardType stype;
	private String xtype;
	private String subtype;
	private List<Parameter> parameters = Collections.<Parameter> emptyList();
	private static final char QUOTE = '"';
	private static final char BS = '\\';

	private static final String TSPECIAL = "()<>@,;:\\\"/[]?=";
	private static final String TC = "[\\p{ASCII}&&[^\\p{Cntrl}" + Pattern.quote(TSPECIAL) + "]]";
	/*
	 * private static final String TC = "[\\p{ASCII}&&[^X]]";
	 */
	private static final Pattern TOKEN = Pattern.compile(TC + "+");

	/**
	 * Constructs a new ContentType.
	 */
	public ContentType(String type, String subtype) {
		this(type, subtype, null);
	}

	/**
	 * Constructs a new ContentType.
	 * 
	 * @param type
	 *            non-null type
	 * @param subtype
	 *            non-null subtype
	 * @param param
	 *            list of parameters, optionally null
	 */
	public ContentType(String type, String subtype, List<Parameter> param) {
		if (type == null)
			throw new NullPointerException("type");
		if (subtype == null)
			throw new NullPointerException("type");
		type = lc(type);
		if (type.startsWith("x-")) {
			checkToken(type);
			this.xtype = type;
		} else {
			stype = StandardType.valueOf(type);
		}
		checkToken(subtype);
		this.subtype = subtype;
		if (param != null)
			this.parameters = new ArrayList<Parameter>(param);
	}

	private static class ParseState {
		int at;
	}

	/**
	 * Factory method, parsing a content type line and generating a content type
	 * object.
	 * 
	 * @return newly created content type object
	 */
	public static ContentType parse(String string) {
		int slash = string.indexOf('/');
		if (slash == -1)
			throw new IllegalArgumentException("No / found: " + string);
		String type = string.substring(0, slash);
		String subtype = null;
		int semi = string.indexOf((char) ';');
		if (semi == -1) {
			subtype = string.substring(slash + 1).trim();
    		return new ContentType(type, subtype);
		}
		ArrayList<Parameter> p = new ArrayList<Parameter>();
		ParseState state = new ParseState();
		state.at = semi;
		while (state.at < string.length()) {
			if (subtype == null)
				subtype = string.substring(slash + 1, state.at).trim();
			int eq = string.indexOf('=', state.at);
			if (eq == -1)
				throw new IllegalArgumentException("Expected = in: " + string + " at " + state.at);
			String name = string.substring(state.at + 1, eq).trim();
			state.at = eq + 1;
			String value = value(string, state);
			p.add(new Parameter(name, value));
		}
		return new ContentType(type, subtype, p);
	}

	private static String value(String string, ParseState state) {
		boolean quoted = false; // value is quoted
		boolean bs = false;     // last character backslash 
		boolean first = true;   // true for first character
		StringBuilder sb = new StringBuilder(); // result
		while (state.at < string.length()) {
			char c = string.charAt(state.at++);
			if (c == BS && quoted) {
				bs = true;
				continue;
			}
			if (c == QUOTE) {
				if (bs) {
					bs = false;
					sb.append(QUOTE);
					continue;
				}
				if (first) {
					quoted = true;
				} else {
					return sb.toString();
				}
			} else if (c == ';' && !quoted) {
				return sb.toString();
			} else {
				sb.append(c);
				bs = false;
				first = false;
			}
		}
		return sb.toString();
	}

	/**
	 * Checks a token syntax.
	 * 
	 * @throws IllegalArgumentException
	 *             if token is invalid
	 */
	public static void checkToken(String t) {
		Matcher matcher = TOKEN.matcher(t);
		if (!matcher.matches())
			throw new IllegalArgumentException("Invalid token: '" + t + "'");
	}

	/**
	 * Content type parameter, see <code>parameter</code> definition in RFC 2045.
	 * Immutable class.
	 * 
	 * @author Elias Ross
	 */
	public final static class Parameter {

		private String attribute;
		private String value;
		
		/**
		 * Lazily created.
		 */
		private String quoteValue;

		/**
		 * Constructs a new Parameter.
		 * 
		 * @param attribute valid attribute token
		 * @param value unquoted, raw value
		 * 
		 * @throws IllegalArgumentException if attribute or value is invalid
		 */
		public Parameter(String attribute, String value) {
			if (attribute == null)
				throw new NullPointerException();
			if (value == null)
				throw new NullPointerException();
			checkToken(attribute);
			this.attribute = attribute;
			this.value = value;
		}

		private void quoteValue() {
			StringBuilder sb = new StringBuilder();
			sb.append(QUOTE);
			boolean quote = false;
			for (int i = 0; i < value.length(); i++) {
				char c = value.charAt(i);
				if (c > 127)
					throw new IllegalArgumentException("Not ASCII " + value);
				if (c == QUOTE) {
					quote = true;
					sb.append('\\');
				} else if (c == '\r' || c == ' ' || c == '\t') {
					quote = true;
				} else if (TSPECIAL.indexOf(c) != -1) {
					quote = true;
				} 
				sb.append(c);
			}
			if (quote)
				quoteValue = sb.append(QUOTE).toString();
			else
				quoteValue = value;
		}

		/**
		 * Returns attribute.
		 */
		public String getAttribute() {
			return attribute;
		}

		/**
		 * Returns value, unquoted.
		 */
		public String getValue() {
			return value;
		}

		/**
		 * Returns the quoted value of this parameter.
		 */
		public String getQuoteValue() {
			if (quoteValue == null)
				quoteValue();
			return quoteValue;
		}

		/**
		 * Returns a formatted <code>attribute=value</code> string.
		 */
		public String toString() {
			return getAttribute() + "=" + getQuoteValue();
		}

	}

	/**
	 * Returns type.
	 */
	public String getType() {
		if (stype != null)
			return stype.name();
		return xtype;
	}

	/**
	 * Returns content sub type.
	 */
	public String getSubtype() {
		return subtype;
	}

	/**
	 * Returns parameters, unmodifiable.
	 */
	public List<Parameter> getParameters() {
		return Collections.unmodifiableList(parameters);
	}
	
	/**
	 * Returns the first parameter matching this attribute string.
	 * Returns null if not found.
	 */
	public Parameter getParameter(String attribute) {
		attribute = lc(attribute);
		for (Parameter p : parameters)
			if (p.getAttribute().equals(attribute))
				return p;
		return null;
	}
	
	/**
	 * Returns the first parameter value matching this attribute string.
	 * Returns null if not found.
	 */
	public String getParameterValue(String attribute) {
		Parameter parameter = getParameter(attribute);
		if (parameter == null)
			return null;
		return parameter.getValue();
	}

	public String toString() {
		StringBuilder sb = new StringBuilder(getType());
		sb.append('/').append(subtype);
		for (Parameter p : parameters)
			sb.append(';').append(p);
		return sb.toString();
	}

	private static String lc(String s) {
		return s.toLowerCase(Locale.ENGLISH);
	}

}