/*
 * 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.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.mail.internet.MimeUtility;
import javax.servlet.ServletRequest;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import nuts.core.lang.ArrayUtils;
import nuts.core.lang.CharsetUtils;
import nuts.core.lang.ExceptionUtils;
import nuts.core.lang.JsonUtils;
import nuts.core.lang.StringUtils;
import nuts.core.net.HttpHeaderDefine;

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

/**
 * utility class for http servlet
 */
public class HttpServletUtils {

	/**
	 * Standard Servlet 2.3+ spec request attributes for include URI and paths.
	 * <p>If included via a RequestDispatcher, the current resource will see the
	 * originating request. Its own URI and paths are exposed as request attributes.
	 */
	public static final String INCLUDE_REQUEST_URI_ATTRIBUTE = "javax.servlet.include.request_uri";
	public static final String INCLUDE_CONTEXT_PATH_ATTRIBUTE = "javax.servlet.include.context_path";
	public static final String INCLUDE_SERVLET_PATH_ATTRIBUTE = "javax.servlet.include.servlet_path";
	public static final String INCLUDE_PATH_INFO_ATTRIBUTE = "javax.servlet.include.path_info";
	public static final String INCLUDE_QUERY_STRING_ATTRIBUTE = "javax.servlet.include.query_string";

	/**
	 * Standard Servlet 2.4+ spec request attributes for forward URI and paths.
	 * <p>If forwarded to via a RequestDispatcher, the current resource will see its
	 * own URI and paths. The originating URI and paths are exposed as request attributes.
	 */
	public static final String FORWARD_REQUEST_URI_ATTRIBUTE = "javax.servlet.forward.request_uri";
	public static final String FORWARD_CONTEXT_PATH_ATTRIBUTE = "javax.servlet.forward.context_path";
	public static final String FORWARD_SERVLET_PATH_ATTRIBUTE = "javax.servlet.forward.servlet_path";
	public static final String FORWARD_PATH_INFO_ATTRIBUTE = "javax.servlet.forward.path_info";
	public static final String FORWARD_QUERY_STRING_ATTRIBUTE = "javax.servlet.forward.query_string";

	/**
	 * Standard Servlet 2.3+ spec request attributes for error pages.
	 * <p>To be exposed to JSPs that are marked as error pages, when forwarding
	 * to them directly rather than through the servlet container's error page
	 * resolution mechanism.
	 */
	public static final String ERROR_STATUS_CODE_ATTRIBUTE = "javax.servlet.error.status_code";
	public static final String ERROR_EXCEPTION_TYPE_ATTRIBUTE = "javax.servlet.error.exception_type";
	public static final String ERROR_MESSAGE_ATTRIBUTE = "javax.servlet.error.message";
	public static final String ERROR_EXCEPTION_ATTRIBUTE = "javax.servlet.error.exception";
	public static final String ERROR_REQUEST_URI_ATTRIBUTE = "javax.servlet.error.request_uri";
	public static final String ERROR_SERVLET_NAME_ATTRIBUTE = "javax.servlet.error.servlet_name";

	public static final String CONTENT_TYPE_TEXT_HTML = "text/html";
	public static final String CONTENT_TYPE_TEXT_XML = "text/xml";
	
	/**
	 * Prefix of the charset clause in a content type String: "charset="
	 */
	public static final String CONTENT_TYPE_CHARSET_PREFIX = "charset=";

	/**
	 * Default character encoding to use when <code>request.getCharacterEncoding</code>
	 * returns <code>null</code>, according to the Servlet spec.
	 */
	public static final String DEFAULT_CHARACTER_ENCODING = "ISO-8859-1";

	/**
	 * Standard Servlet spec context attribute that specifies a temporary
	 * directory for the current web application, of type <code>java.io.File</code>.
	 */
	public static final String TEMP_DIR_CONTEXT_ATTRIBUTE = "javax.servlet.context.tempdir";

	/**
	 * @param request request
	 * @return requestURI
	 */
	public static String getRequestURL(HttpServletRequest request) {
		String url;
		String uri = (String)request.getAttribute(FORWARD_REQUEST_URI_ATTRIBUTE);
		if (StringUtils.isEmpty(uri)) {
			url = request.getRequestURL().toString();
		}
		else {
			String qs = (String)request.getAttribute(FORWARD_QUERY_STRING_ATTRIBUTE);
			if (StringUtils.isEmpty(qs)) {
				url = uri;
			}
			else {
				url = uri + '?' + qs;
			}
		}
		return url;
	}
	
	/**
	 * @param request request
	 * @return requestURI
	 */
	public static String getRequestURI(HttpServletRequest request) {
		String uri = (String)request.getAttribute(FORWARD_REQUEST_URI_ATTRIBUTE);
		if (StringUtils.isEmpty(uri)) {
			uri = request.getRequestURI();
		}
		return uri;
	}
	
	/**
	 * @param request request
	 * @return relative URI
	 */
	public static String getRelativeURI(HttpServletRequest request) {
		if (request == null) {
			return null;
		}
		return getRequestURI(request).substring(request.getContextPath().length());
	}

	public static void logException(Throwable e, HttpServletRequest request) {
		logException(e, request, null);
	}
	
	public static void logException(Throwable e, HttpServletRequest request, String msg) {
		Log log = LogFactory.getLog(e.getClass());

		StringBuilder sb = new StringBuilder();
		if (StringUtils.isNotEmpty(msg)) {
			sb.append(msg).append(StringUtils.LINE_SEPARATOR);
		}
		if (request != null) {
			sb.append(HttpServletUtils.dumpRequestTrace(request));
			sb.append(StringUtils.LINE_SEPARATOR);
		}
		sb.append(ExceptionUtils.getStackTrace(e));

		log.error(sb.toString());
	}
	
	/**
	 * dump request trace
	 * @param request request 
	 * @return string
	 */
	public static String dumpRequestTrace(HttpServletRequest request) {
		StringBuilder sb = new StringBuilder();
		sb.append(request.getRemoteAddr());
		sb.append(" -> ");
		sb.append(request.getRequestURL());

		sb.append(" - ").append(dumpRequestHeaders(request));
		if (!request.getParameterMap().isEmpty()) {
			sb.append(" - ").append(dumpRequestParameters(request));
		}
		if (!ArrayUtils.isEmpty(request.getCookies())) {
			sb.append(" - ").append(dumpRequestCookies(request));
		}
		return sb.toString();
    }

	/**
	 * dump request debug
	 * @param request request 
	 * @return string
	 */
	public static String dumpRequestDebug(HttpServletRequest request) {
		StringBuilder sb = new StringBuilder();
		sb.append(request.getRemoteAddr());
		sb.append(" -> ");
		sb.append(request.getRequestURL());

		if (!request.getParameterMap().isEmpty()) {
			sb.append(" - ").append(dumpRequestParameters(request));
		}
		if (!ArrayUtils.isEmpty(request.getCookies())) {
			sb.append(" - ").append(dumpRequestCookies(request));
		}
		return sb.toString();
    }

	/**
	 * dump request info
	 * @param request request 
	 * @return string
	 */
	public static String dumpRequestInfo(HttpServletRequest request) {
		StringBuilder sb = new StringBuilder();
		sb.append(request.getRemoteAddr());
		sb.append(" -> ");
		sb.append(request.getRequestURL());
		return sb.toString();
    }

	/**
	 * dump request attributes
	 * @param request request
	 * @return attributes dump string
	 */
	public static String dumpRequestAttributes(HttpServletRequest request) {
		if (request == null)
			return "";

		Map<String, Object> map = new HashMap<String, Object>();
		for (Enumeration e = request.getAttributeNames(); e.hasMoreElements(); ) {
			String key = (String)e.nextElement();
			Object value = request.getAttribute(key);
			map.put(key, value);
		}
		return JsonUtils.toJson(map, 2);
	}

	/**
	 * dump request headers
	 * @param request request
	 * @return header dump string
	 */
	public static String dumpRequestHeaders(HttpServletRequest request) {
		if (request == null) {
			return "";
		}
		
		Map<String, List<String>> m = new HashMap<String, List<String>>();
		for (Enumeration e = request.getHeaderNames(); e.hasMoreElements(); ) {
			String n = (String)e.nextElement();
			List<String> v = new ArrayList<String>();
			for (Enumeration e2 = request.getHeaders(n); e2.hasMoreElements(); ) {
				v.add(String.valueOf(e2.nextElement()));
			}
			m.put(n,  v);
		}
		return JsonUtils.toJson(m, 2);
	}

	/**
	 * dump request parameters
	 * @param request request
	 * @return parameters dump string
	 */
	public static String dumpRequestParameters(ServletRequest request) {
		if (request == null) {
			return "";
		}
		
		Map<?,?> pm = request.getParameterMap();
		return JsonUtils.toJson(pm, 2);
	}

	/**
	 * dump request cookies
	 * @param request request
	 * @return cookies dump string
	 */
	public static String dumpRequestCookies(HttpServletRequest request) {
		if (request == null) {
			return "";
		}
		
		Map<String, String> cs = getCookies(request);
		return JsonUtils.toJson(cs, 2);
	}

	/**
	 * get cookies
	 * 
	 * @param request request
	 * @return Cookie map
	 */
	public static Map<String, String> getCookies(HttpServletRequest request) {
		Map<String, String> cm = new HashMap<String, String>();
		Cookie[] cs = request.getCookies();
		if (cs != null) {
			for (Cookie c : cs) {
				if (c != null) {
					cm.put(c.getName(), c.getValue());
				}
			}
		}
		return cm;
	}

	/**
	 * get cookie map
	 * 
	 * @param request request
	 * @return Cookie map
	 */
	public static Map<String, Cookie> getCookieMap(HttpServletRequest request) {
		Map<String, Cookie> cm = new HashMap<String, Cookie>();
		Cookie[] cs = request.getCookies();
		if (cs != null) {
			for (Cookie c : cs) {
				if (c != null) {
					cm.put(c.getName(), c);
				}
			}
		}
		return cm;
	}

	/**
	 * getCookie will return the cookie object that has the name in the Cookies
	 * of the request
	 * 
	 * @param request request
	 * @param name the cookie's name
	 * @return Cookie if no such cookie, return null
	 */
	public static Cookie getCookie(HttpServletRequest request, String name) {
		Cookie[] cookies = request.getCookies();
		if (cookies != null) {
			for (Cookie c : cookies) {
				if (c.getName().equals(name)) {
					return c;
				}
			}
		}
		return null;
	}

	/**
	 * getCookie will return the cookie object that has the name in the Cookies
	 * of the request
	 * 
	 * @param request request
	 * @param name the cookie's name
	 * @return Cookie value if no such cookie, return null
	 */
	public static String getCookieValue(HttpServletRequest request, String name) {
		Cookie[] cookies = request.getCookies();
		if (cookies != null) {
			for (Cookie c : cookies) {
				if (c.getName().equals(name)) {
					return c.getValue();
				}
			}
		}
		return null;
	}

	/**
	 * remove cookie to response
	 * 
	 * @param response response
	 * @param name the cookie's name
	 */
	public static void removeCookie(HttpServletResponse response, String name, String path) {
		Cookie c = new Cookie(name, "");
		c.setMaxAge(0);
		c.setPath(path);
		response.addCookie(c);
	}

	/**
	 * encode file name by User-Agent
	 * @param request request
	 * @param filename file name
	 * @return encoded file name
	 * @throws UnsupportedEncodingException  if an error occurs
	 */
	public static String EncodeFileName(HttpServletRequest request, String filename) throws UnsupportedEncodingException {
		final String enc = CharsetUtils.UTF_8;
		if (request == null) {
			return URLEncoder.encode(filename, enc);
		}
		
		UserAgent ua = new UserAgent(request);
		if (ua.isChrome() || ua.isFirefox()) {
			return MimeUtility.encodeWord(filename, enc, "B");
		}
		else if (ua.isMsie()) {
			return URLEncoder.encode(filename, enc);
		}
		else if (ua.isSafari()) {
			return filename;
		}
		else {
			return MimeUtility.encodeWord(filename, enc, "B");
		}
	}

	/**
	 * Set no cache to response header
	 * @param response HttpServletResponse
	 */
	public static void setResponseNoCache(HttpServletResponse response) {
		response.setHeader(HttpHeaderDefine.CACHE_CONTROL, "no-cache");
		response.setHeader(HttpHeaderDefine.PRAGMA, "no-cache");
		response.setHeader(HttpHeaderDefine.EXPIRES, "0");
	}

	/**
	 * Set response header
	 * @param response HttpServletResponse
	 * @param params params map
	 * @throws IOException if an I/O error occurs
	 */
	public static void setResponseHeader(
			HttpServletResponse response, 
			Map<?, ?> params) throws IOException {
		setResponseHeader(null, response, params);
	}

	/**
	 * Set response header
	 * @param request HttpServletRequest
	 * @param response HttpServletResponse
	 * @param params params map
	 * @throws IOException if an I/O error occurs
	 */
	public static void setResponseHeader(HttpServletRequest request, 
			HttpServletResponse response, 
			Map<?, ?> params) throws IOException {

		HttpServletSupport hss = new HttpServletSupport(request, response);

		hss.setBom(Boolean.TRUE.equals(params.get("bom")));
		hss.setCharset((String)params.get("charset"));
		hss.setContentType((String)params.get("contentType"));
		hss.setContentLength((Integer)params.get("contentLength"));
		hss.setFileName((String)params.get("fileName"));
		if (StringUtils.isEmpty(hss.getFileName())) {
			hss.setFileName((String)params.get("filename"));
		}
		
		hss.setAttachment(Boolean.TRUE.equals(params.get("attachment")));
		hss.setNoCache(Boolean.FALSE.equals(params.get("cache")) || Boolean.TRUE.equals(params.get("noCache")));
		
		hss.writeResponseHeader();
	}

	/**
	 * @param res response 
	 * @param url redirect url
	 * @throws IOException if an I/O error occurs
	 */
	public static void sendRedirect(HttpServletResponse res, String url) throws IOException {
		sendRedirect(res, url, false);
	}

	/**
	 * @param res response 
	 * @param url redirect url
	 * @throws IOException if an I/O error occurs
	 */
	public static void sendRedirect(HttpServletResponse res, String url, boolean permanently) throws IOException {
		res.setStatus(permanently ? HttpServletResponse.SC_MOVED_PERMANENTLY : HttpServletResponse.SC_MOVED_TEMPORARILY);
		res.setHeader(HttpHeaderDefine.LOCATION, res.encodeRedirectURL(url));
	}

	/**
	 * @param res response 
	 * @param url redirect url
	 * @throws IOException if an I/O error occurs
	 */
	public static void writeRedirect(HttpServletResponse res, String url) throws IOException {
		writeRedirect(res, url, true);
	}
	
	/**
	 * @param res response 
	 * @param url redirect url
	 * @throws IOException if an I/O error occurs
	 */
	public static void writeRedirect(HttpServletResponse res, String url, boolean encode) throws IOException {
		HttpServletSupport hss = new HttpServletSupport(res);
		hss.setCharset(CharsetUtils.UTF_8);
		hss.setContentType("text/html");
		hss.setNoCache(true);
		hss.writeResponseHeader();
		
		PrintWriter pw = res.getWriter();
		pw.write("<html><head><meta http-equiv=\"refresh\" content=\"0; url=");
		if (encode) {
			url = res.encodeRedirectURL(url);
		}
		pw.write(url);
		pw.write("\"></head><body></body></html>");
		pw.flush();
	}
	
	
	/** Set up status code to "reason phrase" map. */
	private static Map<Integer, String> statusReasons = new HashMap<Integer, String>();
	
	static {
		// HTTP 1.0 Server status codes -- see RFC 1945
		setStatusReason(HttpServletResponse.SC_OK, "OK");
		setStatusReason(HttpServletResponse.SC_CREATED, "Created");
		setStatusReason(HttpServletResponse.SC_ACCEPTED, "Accepted");
		setStatusReason(HttpServletResponse.SC_NO_CONTENT, "No Content");
		setStatusReason(HttpServletResponse.SC_MOVED_PERMANENTLY, "Moved Permanently");
		setStatusReason(HttpServletResponse.SC_MOVED_TEMPORARILY, "Moved Temporarily");
//		setStatusReason(HttpServletResponse.SC_FOUND, "Found");
		setStatusReason(HttpServletResponse.SC_NOT_MODIFIED, "Not Modified");
		setStatusReason(HttpServletResponse.SC_BAD_REQUEST, "Bad Request");
		setStatusReason(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
		setStatusReason(HttpServletResponse.SC_FORBIDDEN, "Forbidden");
		setStatusReason(HttpServletResponse.SC_NOT_FOUND, "Not Found");
		setStatusReason(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal Server Error");
		setStatusReason(HttpServletResponse.SC_NOT_IMPLEMENTED, "Not Implemented");
		setStatusReason(HttpServletResponse.SC_BAD_GATEWAY, "Bad Gateway");
		setStatusReason(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "Service Unavailable");

		// HTTP 1.1 Server status codes -- see RFC 2048
		setStatusReason(HttpServletResponse.SC_CONTINUE, "Continue");
		setStatusReason(HttpServletResponse.SC_TEMPORARY_REDIRECT, "Temporary Redirect");
		setStatusReason(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "Method Not Allowed");
		setStatusReason(HttpServletResponse.SC_CONFLICT, "Conflict");
		setStatusReason(HttpServletResponse.SC_PRECONDITION_FAILED, "Precondition Failed");
		// setStatusReason(HttpServletResponse.SC_REQUEST_TOO_LONG,
		// "Request Too Long");
		setStatusReason(HttpServletResponse.SC_REQUEST_URI_TOO_LONG, "Request-URI Too Long");
		setStatusReason(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE, "Unsupported Media Type");
		setStatusReason(HttpServletResponse.SC_MULTIPLE_CHOICES, "Multiple Choices");
		setStatusReason(HttpServletResponse.SC_SEE_OTHER, "See Other");
		setStatusReason(HttpServletResponse.SC_USE_PROXY, "Use Proxy");
		setStatusReason(HttpServletResponse.SC_PAYMENT_REQUIRED, "Payment Required");
		setStatusReason(HttpServletResponse.SC_NOT_ACCEPTABLE, "Not Acceptable");
		setStatusReason(HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED, "Proxy Authentication Required");
		setStatusReason(HttpServletResponse.SC_REQUEST_TIMEOUT, "Request Timeout");

		setStatusReason(HttpServletResponse.SC_SWITCHING_PROTOCOLS, "Switching Protocols");
		setStatusReason(HttpServletResponse.SC_NON_AUTHORITATIVE_INFORMATION, "Non Authoritative Information");
		setStatusReason(HttpServletResponse.SC_RESET_CONTENT, "Reset Content");
		setStatusReason(HttpServletResponse.SC_PARTIAL_CONTENT, "Partial Content");
		setStatusReason(HttpServletResponse.SC_GATEWAY_TIMEOUT, "Gateway Timeout");
		setStatusReason(HttpServletResponse.SC_HTTP_VERSION_NOT_SUPPORTED, "Http Version Not Supported");
		setStatusReason(HttpServletResponse.SC_GONE, "Gone");
		setStatusReason(HttpServletResponse.SC_LENGTH_REQUIRED, "Length Required");
		setStatusReason(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE, "Requested Range Not Satisfiable");
		setStatusReason(HttpServletResponse.SC_EXPECTATION_FAILED, "Expectation Failed");

		// // WebDAV Server-specific status codes
		// setStatusReason(HttpServletResponse.SC_PROCESSING,
		// "Processing");
		// setStatusReason(HttpServletResponse.SC_MULTI_STATUS,
		// "Multi-Status");
		// setStatusReason(HttpServletResponse.SC_UNPROCESSABLE_ENTITY,
		// "Unprocessable Entity");
		// setStatusReason(HttpServletResponse.SC_INSUFFICIENT_SPACE_ON_RESOURCE,
		// "Insufficient Space On Resource");
		// setStatusReason(HttpServletResponse.SC_METHOD_FAILURE,
		// "Method Failure");
		// setStatusReason(HttpServletResponse.SC_LOCKED,
		// "Locked");
		// setStatusReason(HttpServletResponse.SC_INSUFFICIENT_STORAGE,
		// "Insufficient Storage");
		// setStatusReason(HttpServletResponse.SC_FAILED_DEPENDENCY,
		// "Failed Dependency");
	}

	private static void setStatusReason(int status, String reason) {
		statusReasons.put(status, reason);
	}

	public static String getStatusReason(int status) {
		String reason = statusReasons.get(status);
		return reason == null ? "UNKNOWN" : reason;
	}
}
