/**
 * Copyright (c) 2000-2013 Liferay, Inc. All rights reserved.
 *
 * The contents of this file are subject to the terms of the Liferay Enterprise
 * Subscription License ("License"). You may not use this file except in
 * compliance with the License. You can obtain a copy of the License by
 * contacting Liferay, Inc. See the License for the specific language governing
 * permissions and limitations under the License, including but not limited to
 * distribution rights of the Software.
 *
 *
 *
 */

package com.liferay.portal.kernel.servlet;

import com.liferay.portal.kernel.util.CharPool;
import com.liferay.portal.kernel.util.StringBundler;
import com.liferay.portal.kernel.util.StringPool;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serializable;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

/**
 * @author Shuyang Zhou
 */
public class MetaInfoCacheServletResponse extends HttpServletResponseWrapper {

	@SuppressWarnings("deprecation")
	public static void finishResponse(
			MetaData metaInfoDataBag, HttpServletResponse response)
		throws IOException {

		if (response.isCommitted()) {
			return;
		}

		resetThrough(response);

		for (Map.Entry<String, Set<Header>> entry :
				metaInfoDataBag._headers.entrySet()) {

			String key = entry.getKey();

			boolean first = true;

			for (Header header : entry.getValue()) {
				if (first) {
					header.setToResponse(key, response);

					first = false;
				}
				else {
					header.addToResponse(key, response);
				}
			}
		}

		if (metaInfoDataBag._location != null) {
			response.sendRedirect(metaInfoDataBag._location);
		}
		else if (metaInfoDataBag._error) {
			response.sendError(
				metaInfoDataBag._status, metaInfoDataBag._errorMessage);
		}
		else {
			if (metaInfoDataBag._charsetName != null) {
				response.setCharacterEncoding(metaInfoDataBag._charsetName);
			}

			if (metaInfoDataBag._contentLength != -1) {
				response.setContentLength(metaInfoDataBag._contentLength);
			}

			if (metaInfoDataBag._contentType != null) {
				response.setContentType(metaInfoDataBag._contentType);
			}

			if (metaInfoDataBag._locale != null) {
				response.setLocale(metaInfoDataBag._locale);
			}

			if (metaInfoDataBag._status != SC_OK) {
				response.setStatus(
					metaInfoDataBag._status, metaInfoDataBag._statusMessage);
			}
		}
	}

	public MetaInfoCacheServletResponse(HttpServletResponse response) {
		super(response);
	}

	@Override
	public void addCookie(Cookie cookie) {

		// The correct header name should be "Set-Cookie". Otherwise, the method
		// containsHeader will not able to detect cookies with the correct
		// header name.

		Set<Header> values = _metaData._headers.get(HttpHeaders.SET_COOKIE);

		if (values == null) {
			values = new HashSet<Header>();

			_metaData._headers.put(HttpHeaders.SET_COOKIE, values);
		}

		Header header = new Header(cookie);

		values.add(header);

		super.addCookie(cookie);
	}

	@Override
	public void addDateHeader(String name, long value) {
		Set<Header> values = _metaData._headers.get(name);

		if (values == null) {
			values = new HashSet<Header>();

			_metaData._headers.put(name, values);
		}

		Header header = new Header(value);

		values.add(header);

		super.addDateHeader(name, value);
	}

	@Override
	public void addHeader(String name, String value) {
		if (name.equals(HttpHeaders.CONTENT_TYPE)) {
			setContentType(value);

			return;
		}

		Set<Header> values = _metaData._headers.get(name);

		if (values == null) {
			values = new HashSet<Header>();

			_metaData._headers.put(name, values);
		}

		Header header = new Header(value);

		values.add(header);

		super.addHeader(name, value);
	}

	@Override
	public void addIntHeader(String name, int value) {
		Set<Header> values = _metaData._headers.get(name);

		if (values == null) {
			values = new HashSet<Header>();

			_metaData._headers.put(name, values);
		}

		Header header = new Header(value);

		values.add(header);

		super.addIntHeader(name, value);
	}

	@Override
	public boolean containsHeader(String name) {
		return _metaData._headers.containsKey(name);
	}

	public void finishResponse() throws IOException {
		finishResponse(_metaData, (HttpServletResponse)getResponse());

		_committed = true;
	}

	/**
	 * @deprecated As of 7.0.0, replaced by {@link #finishResponse()}}
	 */
	@Deprecated
	public void finishResponse(boolean reapplyMetaData) throws IOException {
		HttpServletResponse response = (HttpServletResponse)getResponse();

		if (reapplyMetaData) {
			finishResponse(_metaData, response);
		}

		_committed = true;
	}

	@Override
	@SuppressWarnings("unused")
	public void flushBuffer() throws IOException {
		_committed = true;
	}

	@Override
	public int getBufferSize() {
		return _metaData._bufferSize;
	}

	@Override
	public String getCharacterEncoding() {

		// We are supposed to default to ISO-8859-1 based on the Servlet
		// specification. However, most application servers honors the system
		// property "file.encoding". Using the system default character gives us
		// better application server compatibility.

		if (_metaData._charsetName == null) {
			return StringPool.DEFAULT_CHARSET_NAME;
		}

		return _metaData._charsetName;
	}

	@Override
	public String getContentType() {
		String contentType = _metaData._contentType;

		if ((contentType != null) && (_metaData._charsetName != null)) {
			contentType = contentType.concat("; charset=").concat(
				_metaData._charsetName);
		}

		return contentType;
	}

	/**
	 * When the header for this given name is "Cookie", the return value cannot
	 * be used for the "Set-Cookie" header. The string representation for
	 * "Cookie" is application server specific. The only safe way to add the
	 * header is to call {@link HttpServletResponse#addCookie(Cookie)}.
	 */
	@Override
	public String getHeader(String name) {
		Set<Header> values = _metaData._headers.get(name);

		if (values == null) {
			return null;
		}

		Header header = values.iterator().next();

		return header.toString();
	}

	@Override
	public Collection<String> getHeaderNames() {
		return _metaData._headers.keySet();
	}

	public Map<String, Set<Header>> getHeaders() {
		return _metaData._headers;
	}

	/**
	 * When the header for this given name is "Cookie", the return value cannot
	 * be used for the "Set-Cookie" header. The string representation for
	 * "Cookie" is application server specific. The only safe way to add the
	 * header is to call {@link HttpServletResponse#addCookie(Cookie)}.
	 */
	@Override
	public Collection<String> getHeaders(String name) {
		Set<Header> values = _metaData._headers.get(name);

		if (values == null) {
			return Collections.emptyList();
		}

		List<String> stringValues = new ArrayList<String>();

		for (Header header : values) {
			stringValues.add(header.toString());
		}

		return stringValues;
	}

	@Override
	public Locale getLocale() {
		return _metaData._locale;
	}

	public MetaData getMetaData() {
		return _metaData;
	}

	@Override
	public ServletOutputStream getOutputStream() throws IOException {
		calledGetOutputStream = true;

		return super.getOutputStream();
	}

	@Override
	public int getStatus() {
		return _metaData._status;
	}

	@Override
	public PrintWriter getWriter() throws IOException {
		calledGetWriter = true;

		return super.getWriter();
	}

	@Override
	public boolean isCommitted() {
		ServletResponse servletResponse = getResponse();

		return _committed || servletResponse.isCommitted();
	}

	@Override
	public void reset() {
		if (isCommitted()) {
			throw new IllegalStateException("Reset after commit");
		}

		// No need to reset _error, _errorMessage and _location, because setting
		// them requires commit.

		_metaData._charsetName = null;
		_metaData._contentLength = -1;
		_metaData._contentType = null;
		_metaData._headers.clear();
		_metaData._locale = null;
		_metaData._status = SC_OK;
		_metaData._statusMessage = null;

		// calledGetOutputStream and calledGetWriter should be cleared by
		// resetBuffer() in subclass.

		resetBuffer();

		super.reset();
	}

	@Override
	public void resetBuffer() {
		if (isCommitted()) {
			throw new IllegalStateException("Reset buffer after commit");
		}

		resetBuffer(false);
	}

	@Override
	public void sendError(int status) throws IOException {
		if (isCommitted()) {
			throw new IllegalStateException("Send error after commit");
		}

		_metaData._error = true;
		_metaData._status = status;

		resetBuffer();

		_committed = true;

		super.sendError(status);
	}

	@Override
	public void sendError(int status, String errorMessage) throws IOException {
		if (isCommitted()) {
			throw new IllegalStateException("Send error after commit");
		}

		_metaData._error = true;
		_metaData._errorMessage = errorMessage;
		_metaData._status = status;

		resetBuffer();

		_committed = true;

		super.sendError(status, errorMessage);
	}

	@Override
	public void sendRedirect(String location) throws IOException {
		if (isCommitted()) {
			throw new IllegalStateException("Send redirect after commit");
		}

		resetBuffer(true);

		setStatus(SC_FOUND);

		_metaData._location = location;

		_committed = true;

		super.sendRedirect(location);
	}

	@Override
	public void setBufferSize(int bufferSize) {
		if (isCommitted()) {
			throw new IllegalStateException("Set buffer size after commit");
		}

		_metaData._bufferSize = bufferSize;

		super.setBufferSize(bufferSize);
	}

	@Override
	public void setCharacterEncoding(String charsetName) {
		if (isCommitted()) {
			return;
		}

		if (calledGetWriter) {
			return;
		}

		if (charsetName == null) {
			return;
		}

		_metaData._charsetName = charsetName;

		super.setCharacterEncoding(charsetName);
	}

	@Override
	public void setContentLength(int contentLength) {
		if (isCommitted()) {
			return;
		}

		_metaData._contentLength = contentLength;

		super.setContentLength(contentLength);
	}

	@Override
	public void setContentType(String contentType) {
		if (isCommitted()) {
			return;
		}

		if (contentType == null) {
			return;
		}

		int index = contentType.indexOf(CharPool.SEMICOLON);

		if (index != -1) {
			String firstPart = contentType.substring(0, index);

			_metaData._contentType = firstPart.trim();

			index = contentType.indexOf("charset=");

			if (index != -1) {
				String charsetName = contentType.substring(index + 8);

				charsetName = charsetName.trim();

				setCharacterEncoding(charsetName);
			}
			else {
				_metaData._charsetName = null;
			}
		}
		else {
			_metaData._contentType = contentType;

			_metaData._charsetName = null;
		}

		super.setContentType(contentType);
	}

	@Override
	public void setDateHeader(String name, long value) {
		Set<Header> values = new HashSet<Header>();

		_metaData._headers.put(name, values);

		Header header = new Header(value);

		values.add(header);

		super.setDateHeader(name, value);
	}

	@Override
	public void setHeader(String name, String value) {
		if (name.equals(HttpHeaders.CONTENT_TYPE)) {
			setContentType(value);

			return;
		}

		Set<Header> values = new HashSet<Header>();

		_metaData._headers.put(name, values);

		Header header = new Header(value);

		values.add(header);

		super.setHeader(name, value);
	}

	@Override
	public void setIntHeader(String name, int value) {
		Set<Header> values = new HashSet<Header>();

		_metaData._headers.put(name, values);

		Header header = new Header(value);

		values.add(header);

		super.setIntHeader(name, value);
	}

	@Override
	public void setLocale(Locale locale) {
		if (isCommitted()) {
			return;
		}

		_metaData._locale = locale;

		super.setLocale(locale);
	}

	@Override
	public void setStatus(int status) {
		if (isCommitted()) {
			return;
		}

		_metaData._status = status;

		super.setStatus(status);
	}

	@Override
	@SuppressWarnings("deprecation")
	public void setStatus(int status, String statusMessage) {
		if (isCommitted()) {
			return;
		}

		_metaData._status = status;
		_metaData._statusMessage = statusMessage;

		super.setStatus(status, statusMessage);
	}

	@Override
	public String toString() {
		StringBundler sb = new StringBundler(23);

		sb.append("{bufferSize=");
		sb.append(_metaData._bufferSize);
		sb.append(", charsetName=");
		sb.append(_metaData._charsetName);
		sb.append(", committed=");
		sb.append(_committed);
		sb.append(", contentLength=");
		sb.append(_metaData._contentLength);
		sb.append(", contentType=");
		sb.append(_metaData._contentType);
		sb.append(", error=");
		sb.append(_metaData._error);
		sb.append(", errorMessage=");
		sb.append(_metaData._errorMessage);
		sb.append(", headers=");
		sb.append(_metaData._headers);
		sb.append(", location=");
		sb.append(_metaData._location);
		sb.append(", locale=");
		sb.append(_metaData._locale);
		sb.append(", status=");
		sb.append(_metaData._status);
		sb.append("}");

		return sb.toString();
	}

	public static class MetaData implements Serializable {

		private int _bufferSize;
		private String _charsetName;
		private int _contentLength = -1;
		private String _contentType;
		private boolean _error;
		private String _errorMessage;
		private Map<String, Set<Header>> _headers =
			new HashMap<String, Set<Header>>();
		private Locale _locale;
		private String _location;
		private int _status = SC_OK;
		private String _statusMessage;

	}

	protected static void resetThrough(HttpServletResponse response) {
		if (response instanceof MetaInfoCacheServletResponse) {
			MetaInfoCacheServletResponse metaInfoCacheServletResponse =
				(MetaInfoCacheServletResponse)response;

			resetThrough(
				(HttpServletResponse)
					metaInfoCacheServletResponse.getResponse());
		}
		else {
			response.reset();
		}
	}

	/**
	 * Stub method for subclass to provide buffer resetting logic.
	 *
	 * @param nullOutReferences whether to reset flags. It is not directly used
	 *        by this class. Subclasses with an actual buffer may behave
	 *        differently depending on the value of this parameter.
	 */
	protected void resetBuffer(boolean nullOutReferences) {
		super.resetBuffer();
	}

	protected boolean calledGetOutputStream;
	protected boolean calledGetWriter;

	private boolean _committed;
	private MetaData _metaData = new MetaData();

}