/*
 * This file is part of Nuts Framework.
 * Copyright(C) 2009-2012 Nuts Develop Team.
 *
 * Nuts Framework 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 3 of the License any later version.
 * 
 * Nuts Framework 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 Nuts Framework. If not, see <http://www.gnu.org/licenses/>.
 */
package nuts.core.servlet;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import nuts.core.lang.CharsetUtils;
import nuts.core.lang.StringUtils;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * URLHelper
 */
public class URLHelper {
	/**
	 * log
	 */
	private static final Log log = LogFactory.getLog(URLHelper.class);

	/**
	 * URL_ENCODE = "UTF-8";
	 */
	private static final String URL_ENCODE = CharsetUtils.UTF_8;

	/**
	 * get schema from url
	 * @param url url string
	 * @return schema
	 */
	public static String getUrlSchema(String url) {
		if (StringUtils.isEmpty(url)) {
			return url;
		}

		String schema = null;
		int i = url.indexOf("://");
		if (i >= 0) {
			schema = url.substring(0, i);
		}
		
		return schema;
	}
	
	/**
	 * get domain from url
	 * @param url url string
	 * @return domain
	 */
	public static String getUrlDomain(String url) {
		if (StringUtils.isEmpty(url)) {
			return url;
		}
		
		int i = url.indexOf("://");
		if (i >= 0) {
			url = StringUtils.stripStart(url.substring(i + 3), "/");
		}

		i = url.indexOf('/');
		if (i >= 0) {
			url = url.substring(0, i);
		}
		
		i = url.indexOf(':');
		if (i >= 0) {
			url = url.substring(0, i);
		}

		return url;
	}
	
	/**
	 * get root from url
	 * @param url url string
	 * @return root
	 */
	public static String getUrlRoot(String url) {
		if (StringUtils.isEmpty(url)) {
			return url;
		}
		
		int i = url.indexOf("://");
		i = url.indexOf('/', i);
		if (i >= 0) {
			url = url.substring(0, i);
		}

		return url;
	}
	
	/**
	 * build the request URL, append request parameters as query string
	 * 
	 * @param request http request
	 * @return URL
	 */
	public static String buildURL(HttpServletRequest request) {
		return buildURL(request, request.getParameterMap());
	}

	/**
	 * build the request URL, append parameters as query string
	 * 
	 * @param request http request
	 * @param params parameters
	 * @return URL
	 */
	public static String buildURL(HttpServletRequest request, Map params) {
		return buildURL(request, params, false);
	}

	/**
	 * build the request URL, append parameters as query string
	 * 
	 * @param request http request
	 * @param params parameters
	 * @param escapeAmp escape &
	 * @return URL
	 */
	public static String buildURL(HttpServletRequest request, Map params, boolean escapeAmp) {
		return buildURL(request, params, false, escapeAmp);
	}

	/**
	 * build the request URL, append parameters as query string
	 * 
	 * @param request http request
	 * @param params parameters
	 * @param forceAddSchemeHostAndPort add schema and port
	 * @param escapeAmp escape &
	 * @return URL
	 */
	public static String buildURL(HttpServletRequest request, Map params, boolean forceAddSchemeHostAndPort, boolean escapeAmp) {
		return buildURL(request, null, params, null, 0, forceAddSchemeHostAndPort, escapeAmp);
	}

	/**
	 * build the request URL, append parameters as query string
	 * 
	 * @param request http request
	 * @param params parameters
	 * @param scheme scheme
	 * @param port port
	 * @param forceAddSchemeHostAndPort add schema and port
	 * @param escapeAmp escape &
	 * @return URL
	 */
    public static String buildURL(HttpServletRequest request, Map params, String scheme, int port, boolean forceAddSchemeHostAndPort, boolean escapeAmp) {
		return buildURL(request, null, params, scheme, port, forceAddSchemeHostAndPort, escapeAmp);
    }
    
	/**
	 * build the request URL, append parameters as query string
	 * 
	 * @param uri request uri
	 * @param params parameters
	 * @return URL
	 */
	public static String buildURL(String uri, Map params) {
		return buildURL(null, uri, params, null, 0, false, false);
	}

	/**
	 * build the request URL, append parameters as query string
	 * 
	 * @param uri request uri
	 * @param params parameters
	 * @param escapeAmp escape &
	 * @return URL
	 */
	public static String buildURL(String uri, Map params, boolean escapeAmp) {
		return buildURL(null, uri, params, null, 0, false, escapeAmp);
	}

	/**
	 * build the request URL, append parameters as query string
	 * 
	 * @param request http request
	 * @param uri request uri
	 * @param params parameters
	 * @param scheme scheme
	 * @param port port
	 * @param forceAddSchemeHostAndPort add schema and port
	 * @param escapeAmp escape &
	 * @return URL
	 */
    public static String buildURL(HttpServletRequest request, String uri, Map params, String scheme, int port, boolean forceAddSchemeHostAndPort, boolean escapeAmp) {
		StringBuilder link = new StringBuilder();

		// only append scheme if it is different to the current scheme *OR*
		// if we explicity want it to be appended by having forceAddSchemeHostAndPort = true
		if (forceAddSchemeHostAndPort) {
			String reqScheme = request.getScheme();
			link.append(scheme != null ? scheme : reqScheme);
			link.append("://").append(request.getServerName());

			if (scheme != null) {
				// If switching schemes, use the configured port for the particular scheme.
				if (!scheme.equals(reqScheme)) {
					if (port > 0) {
						link.append(":").append(port);
					}
					// Else use the port from the current request.
				}
				else {
					int reqPort = request.getServerPort();

					if ((scheme.equals("http") && (reqPort != 80))
							|| (scheme.equals("https") && reqPort != 443)) {
						link.append(":").append(reqPort);
					}
				}
			}
		}
		else if ((scheme != null) && !scheme.equals(request.getScheme())) {
			link.append(scheme).append("://").append(request.getServerName());

			if (port > 0) {
				link.append(":").append(port);
			}
		}

		if (uri == null) {
			// Go to "same page"
			// (Applicable to Servlet 2.4 containers)
			// If the request was forwarded, the attribute below will be set with the original URL
			uri = HttpServletUtils.getRequestURI(request);
		}

		link.append(uri);

		// if the action was not explicitly set grab the params from the request
		if (escapeAmp) {
			buildParametersString(params, link, "&amp;");
		}
		else {
			buildParametersString(params, link, "&");
		}

		return link.toString();
    }

	/**
	 * @param params parameter map
	 * @param link link
	 */
	public static void buildParametersString(Map params, StringBuilder link) {
		buildParametersString(params, link, "&");
	}

	/**
	 * @param params parameter map
	 * @param link link
	 * @param paramSeparator parameter separator
	 */
	public static void buildParametersString(Map params, StringBuilder link, String paramSeparator) {
		if ((params != null) && (params.size() > 0)) {
			if (link.toString().indexOf("?") == -1) {
				link.append("?");
			}
			else {
				link.append(paramSeparator);
			}

			// Set params
			Iterator iter = params.entrySet().iterator();

			while (iter.hasNext()) {
				Map.Entry entry = (Map.Entry) iter.next();
				String name = (String) entry.getKey();
				Object value = entry.getValue();

				if (value instanceof Iterable) {
					for (Iterator iterator = ((Iterable) value).iterator(); iterator.hasNext();) {
						Object paramValue = iterator.next();
						link.append(buildParameterSubstring(name, paramValue));

						if (iterator.hasNext()) {
							link.append(paramSeparator);
						}
					}
				}
				else if (value instanceof Object[]) {
					Object[] array = (Object[]) value;
					for (int i = 0; i < array.length; i++) {
						Object paramValue = array[i];
						link.append(buildParameterSubstring(name, paramValue));

						if (i < array.length - 1)
							link.append(paramSeparator);
					}
				}
				else {
					link.append(buildParameterSubstring(name, value));
				}

				if (iter.hasNext())
					link.append(paramSeparator);
			}
		}
	}

	private static String buildParameterSubstring(String name, Object value) {
		StringBuilder builder = new StringBuilder();
		builder.append(name);
		builder.append('=');
		builder.append(value == null ? "" : translateAndEncode(value.toString()));

		return builder.toString();
	}

	/**
	 * Translates any script expressions using
	 * {@link com.opensymphony.xwork2.util.TextParseUtil#translateVariables} and encodes the URL
	 * using {@link java.net.URLEncoder#encode} with the encoding specified in the configuration.
	 * 
	 * @param input
	 * @return the translated and encoded string
	 */
	private static String translateAndEncode(String input) {
		try {
			return URLEncoder.encode(input, URL_ENCODE);
		}
		catch (UnsupportedEncodingException e) {
			log.warn("Could not encode URL parameter '" + input + "', returning value un-encoded");
			return input;
		}
	}

	private static String translateAndDecode(String input) {
		try {
			return URLDecoder.decode(input, URL_ENCODE);
		}
		catch (UnsupportedEncodingException e) {
			log.warn("Could not encode URL parameter '" + input + "', returning value un-encoded");
			return input;
		}
	}

	/**
	 * @param queryString query string
	 * @return parameter map
	 */
	public static Map parseQueryString(String queryString) {
		return parseQueryString(queryString, false);
	}

	/**
	 * @param queryString query string
	 * @param forceValueArray if true each parameter is array
	 * @return parameter map
	 */
	@SuppressWarnings("unchecked")
	public static Map parseQueryString(String queryString, boolean forceValueArray) {
		Map queryParams = new LinkedHashMap();
		if (queryString != null) {
			String[] params = queryString.split("&");
			for (int a = 0; a < params.length; a++) {
				if (params[a].trim().length() > 0) {
					String[] tmpParams = params[a].split("=");
					String paramName = null;
					String paramValue = "";
					if (tmpParams.length > 0) {
						paramName = tmpParams[0];
					}
					if (tmpParams.length > 1) {
						paramValue = tmpParams[1];
					}
					if (paramName != null) {
						String translatedParamValue = translateAndDecode(paramValue);

						if (queryParams.containsKey(paramName) || forceValueArray) {
							// WW-1619 append new param value to existing value(s)
							Object currentParam = queryParams.get(paramName);
							if (currentParam instanceof String) {
								queryParams.put(paramName, new String[] { (String) currentParam,
										translatedParamValue });
							}
							else {
								String currentParamValues[] = (String[]) currentParam;
								if (currentParamValues != null) {
									List paramList = new ArrayList(Arrays
											.asList(currentParamValues));
									paramList.add(translatedParamValue);
									String newParamValues[] = new String[paramList.size()];
									queryParams.put(paramName, paramList.toArray(newParamValues));
								}
								else {
									queryParams.put(paramName,
										new String[] { translatedParamValue });
								}
							}
						}
						else {
							queryParams.put(paramName, translatedParamValue);
						}
					}
				}
			}
		}
		return queryParams;
	}
}
