package org.jboss.as.console.client.core;

import com.google.gwt.http.client.URL;
import com.google.inject.Inject;
import com.gwtplatform.mvp.shared.proxy.PlaceRequest;
import com.gwtplatform.mvp.shared.proxy.TokenFormatException;
import com.gwtplatform.mvp.shared.proxy.TokenFormatter;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * Formats tokens from {@code String} values to {@link PlaceRequest} and {@link PlaceRequest} hierarchies and vice-versa. The default implementation
 * uses:
 * <ul>
 * <li>{@code '/'} to separate {@link PlaceRequest}s in a hierarchy;</li>
 * <li>{@code ';'} to separate parameters in a {@link PlaceRequest};</li>
 * <li>{@code '='} to separate the parameter name from its value;</li>
 * <li>{@code '\'} to escape separators inside parameters names and values in a {@link PlaceRequest}.</li>
 * </ul>
 * These symbols cannot be used in a name token. If one of the separating symbol is encountered in a parameter or a value it is escaped using the
 * {@code '\'} character by replacing {@code '/'} with {@code '\0'}, {@code ';'} with {@code '\1'}, {@code '='} with {@code '\2'} and {@code '\'} with
 * {@code '\3'}.
 * <p />
 * Before decoding a {@link String} URL fragment into a {@link PlaceRequest} or a {@link PlaceRequest} hierarchy, {@link NewTokenFormatter} will first
 * pass the {@link String} through {@link URL#decodeQueryString(String)} so that if the URL was URL-encoded by some user agent, like a mail user
 * agent, it is still parsed correctly.
 * <p />
 * For example, {@link NewTokenFormatter} would parse any of the following:
 * 
 * <pre>
 * nameToken1%3Bparam1.1%3Dvalue1.1%3Bparam1.2%3Dvalue1.2%2FnameToken2%2FnameToken3%3Bparam3.1%3Dvalue%03%11
 * nameToken1;param1.1=value1.1;param1.2=value1.2/nameToken2/nameToken3;param3.1=value\03\21
 * </pre>
 * 
 * Into the following hierarchy of {@link PlaceRequest}:
 * 
 * <pre>
 * {
 *   { "nameToken1", { {"param1.1", "value1.1"}, {"parame1.2","value1.2"} },
 *     "nameToken2", {},
 *     "nameToken3", { {"param3.1", "value/3=1"} } }
 * }
 * </pre>
 * 
 * If you want to use different symbols as separator, use the {@link #ParameterTokenFormatter(String, String, String, String)} constructor.
 * 
 * @author Philippe Beaudoin
 * @author Yannis Gonianakis
 * @author Daniel Colchete
 */
public class NewTokenFormatter implements TokenFormatter {

	protected static final String DEFAULT_HIERARCHY_SEPARATOR = "/";
	protected static final String DEFAULT_PARAM_SEPARATOR = ";";
	protected static final String DEFAULT_VALUE_SEPARATOR = "=";
	protected static final String DEFAULT_ESCAPE_CHARACTER = "\\";

	protected static final String ESCAPED_HIERARCHY_SEPARATOR = "\\0"; // escaped version for the DEFAULT_HIERARCHY_SEPARATOR
	protected static final String ESCAPED_PARAM_SEPARATOR = "\\1"; // escaped version for the DEFAULT_PARAM_SEPARATOR
	protected static final String ESCAPED_VALUE_SEPARATOR = "\\2"; // escaped version for the DEFAULT_VALUE_SEPARATOR
	protected static final String ESCAPED_ESCAPE_CHAR = "\\3"; // escaped version for the DEFAULT_ESCAPE_CHARACTER

	private final String escapeCharacter;
	private final String hierarchySeparator;
	private final String paramSeparator;
	private final String valueSeparator;

	/**
	 * Builds a {@link NewTokenFormatter} using the default separators and escape character.
	 */
	@Inject
	public NewTokenFormatter() {
		this(DEFAULT_HIERARCHY_SEPARATOR, DEFAULT_PARAM_SEPARATOR, DEFAULT_VALUE_SEPARATOR, DEFAULT_ESCAPE_CHARACTER);
	}

	/**
	 * This constructor makes it possible to use custom separators in your token formatter. The separators must be 1-letter strings, they must all be
	 * different from one another, and they must be encoded when ran through {@link URL#encodeQueryString(String)})
	 * 
	 * @param hierarchySeparator
	 *            The symbol used to separate {@link PlaceRequest} in a hierarchy. Must be a 1-character string and can't be {@code %}.
	 * @param paramSeparator
	 *            The symbol used to separate parameters in a {@link PlaceRequest}. Must be a 1-character string and can't be {@code %}.
	 * @param valueSeparator
	 *            The symbol used to separate the parameter name from its value. Must be a 1-character string and can't be {@code %}.
	 */
	public NewTokenFormatter(String hierarchySeparator, String paramSeparator, String valueSeparator) {
		this(hierarchySeparator, paramSeparator, valueSeparator, DEFAULT_ESCAPE_CHARACTER);
	}

	/**
	 * This constructor makes it possible to use custom separators and custom escape character in your token formatter. The separators and the escape
	 * character must be 1-letter strings, they must all be different from one another, and they must be encoded when ran through
	 * {@link URL#encodeQueryString(String)})
	 * 
	 * @param hierarchySeparator
	 *            The symbol used to separate {@link PlaceRequest} in a hierarchy. Must be a 1-character string and can't be {@code %}.
	 * @param paramSeparator
	 *            The symbol used to separate parameters in a {@link PlaceRequest}. Must be a 1-character string and can't be {@code %}.
	 * @param valueSeparator
	 *            The symbol used to separate the parameter name from its value. Must be a 1-character string and can't be {@code %}.
	 * @param escapeCharacter
	 *            The symbol used to escape the separator symbols inside parameter names and values. Must be a 1-character string and can't be
	 *            {@code %}.
	 */
	public NewTokenFormatter(String hierarchySeparator, String paramSeparator, String valueSeparator, String escapeCharacter) {
		assert hierarchySeparator.length() == 1;
		assert paramSeparator.length() == 1;
		assert valueSeparator.length() == 1;
		assert escapeCharacter.length() == 1;
		assert !escapeCharacter.equals(hierarchySeparator);
		assert !escapeCharacter.equals(paramSeparator);
		assert !escapeCharacter.equals(valueSeparator);
		assert !hierarchySeparator.equals(paramSeparator);
		assert !hierarchySeparator.equals(valueSeparator);
		assert !paramSeparator.equals(valueSeparator);
		assert !escapeCharacter.equals(URL.encodeQueryString(escapeCharacter));
		assert !valueSeparator.equals(URL.encodeQueryString(valueSeparator));
		assert !hierarchySeparator.equals(URL.encodeQueryString(hierarchySeparator));
		assert !paramSeparator.equals(URL.encodeQueryString(paramSeparator));
		assert !escapeCharacter.equals("%");
		assert !hierarchySeparator.equals("%");
		assert !paramSeparator.equals("%");
		assert !valueSeparator.equals("%");

		this.hierarchySeparator = hierarchySeparator;
		this.paramSeparator = paramSeparator;
		this.valueSeparator = valueSeparator;
		this.escapeCharacter = escapeCharacter;
	}

	@Override
	public String toHistoryToken(List<PlaceRequest> placeRequestHierarchy) throws TokenFormatException {
		StringBuilder out = new StringBuilder();

		for (int i = 0; i < placeRequestHierarchy.size(); ++i) {
			if (i != 0) {
				out.append(hierarchySeparator);
			}
			out.append(toPlaceTokenUnescaped(placeRequestHierarchy.get(i)));
		}

		return out.toString();
	}

	@Override
	public PlaceRequest toPlaceRequest(String placeToken) throws TokenFormatException {
		return unescapedToPlaceRequest(URL.decodeQueryString(placeToken));
	}

	private PlaceRequest unescapedToPlaceRequest(String unescapedPlaceToken) throws TokenFormatException {
		PlaceRequest req = null;

		int split = unescapedPlaceToken.indexOf(paramSeparator);
		if (split == 0) {
			throw new TokenFormatException("Place history token is missing.");
		} else if (split == -1) {
			req = new PlaceRequest(unescapedPlaceToken);
		} else if (split >= 0) {
			req = new PlaceRequest(unescapedPlaceToken.substring(0, split));
			String paramsChunk = unescapedPlaceToken.substring(split + 1);
			String[] paramTokens = paramsChunk.split(paramSeparator);
			for (String paramToken : paramTokens) {
				if (paramToken.isEmpty()) {
					throw new TokenFormatException("Bad parameter: Successive parameters require a single '" + paramSeparator + "' between them.");
				}
				String[] param = splitParamToken(paramToken);
				String key = paramValueUnescape(param[0]);
				String value = paramValueUnescape(param[1]);
				req = req.with(key, value);
			}
		}
		return req;
	}

	@Override
	public List<PlaceRequest> toPlaceRequestHierarchy(String historyToken) throws TokenFormatException {
		historyToken = URL.decodeQueryString(historyToken);

		int split = historyToken.indexOf(hierarchySeparator);
		if (split == 0) {
			throw new TokenFormatException("Place history token is missing.");
		} else {
			List<PlaceRequest> result = new ArrayList<PlaceRequest>();
			if (split == -1) {
				result.add(unescapedToPlaceRequest(historyToken)); // History token consists of a single place token
			} else {
				String[] placeTokens = historyToken.split(hierarchySeparator);
				for (String placeToken : placeTokens) {
					if (placeToken.isEmpty()) {
						throw new TokenFormatException("Bad parameter: Successive place tokens require a single '" + hierarchySeparator + "' between them.");
					}
					result.add(unescapedToPlaceRequest(placeToken));
				}
			}
			return result;
		}
	}
	
	@Override
	public String toPlaceToken(PlaceRequest placeRequest) throws TokenFormatException {
		return toPlaceTokenUnescaped(placeRequest);
	}

	private String toPlaceTokenUnescaped(PlaceRequest placeRequest) throws TokenFormatException {
		StringBuilder out = new StringBuilder();
		out.append(placeRequest.getNameToken());

		Set<String> params = placeRequest.getParameterNames();
		if (params != null) {
			for (String name : params) {
				out.append(paramSeparator).append(paramValueEscape(name)).append(valueSeparator).append(paramValueEscape(placeRequest.getParameter(name, null)));
			}
		}
		
		return out.toString();
	}

	private String[] splitParamToken(String paramToken) {
		String[] param = paramToken.split(valueSeparator, 2);
		if (param.length == 1 // pattern didn't match
				|| param[0].contains(valueSeparator) // un-escaped separator encountered in the key
				|| param[1].contains(valueSeparator)) { // un-escaped separator encountered in the value
			throw new TokenFormatException("Bad parameter: Parameters require a single '" + valueSeparator + "' between the key and value.");
		}
		return param;
	}

	private String paramValueEscape(String value) {

		StringBuffer sbuf = new StringBuffer();
		int len = value.length();

		char escapeChar = escapeCharacter.charAt(0);
		char hierarchyChar = hierarchySeparator.charAt(0);
		char paramChar = paramSeparator.charAt(0);
		char valueChar = valueSeparator.charAt(0);

		for (int i = 0; i < len; i++) {
			char ch = value.charAt(i);

			if (ch == escapeChar) {
				sbuf.append(ESCAPED_ESCAPE_CHAR);
			} else if (ch == hierarchyChar) {
				sbuf.append(ESCAPED_HIERARCHY_SEPARATOR);
			} else if (ch == paramChar) {
				sbuf.append(ESCAPED_PARAM_SEPARATOR);
			} else if (ch == valueChar) {
				sbuf.append(ESCAPED_VALUE_SEPARATOR);
			} else {
				sbuf.append(ch);
			}
		}

		return URL.encodeQueryString(sbuf.toString());
	}

	private String paramValueUnescape(String value) {
		value = URL.decodeQueryString(value);
		
		StringBuffer sbuf = new StringBuffer();
		int len = value.length();

		char escapeChar = escapeCharacter.charAt(0);

		char hierarchyNum = ESCAPED_HIERARCHY_SEPARATOR.charAt(1);
		char paramNum = ESCAPED_PARAM_SEPARATOR.charAt(1);
		char valueNum = ESCAPED_VALUE_SEPARATOR.charAt(1);
		char escapeNum = ESCAPED_ESCAPE_CHAR.charAt(1);

		for (int i = 0; i < len; i++) {
			char ch = value.charAt(i);

			if (ch == escapeChar) {
				i++;
				char ch2 = value.charAt(i);
				if (ch2 == hierarchyNum) {
					sbuf.append(hierarchySeparator);
				} else if (ch2 == paramNum) {
					sbuf.append(paramSeparator);
				} else if (ch2 == valueNum) {
					sbuf.append(valueSeparator);
				} else if (ch2 == escapeNum) {
					sbuf.append('\\');
				}
			} else {
				sbuf.append(ch);
			}
		} 	

		return sbuf.toString();
	}

}
