/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.wicket.protocol.http;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map.Entry;

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

import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.util.collections.MultiMap;
import org.apache.wicket.util.io.StringBufferWriter;
import org.apache.wicket.util.string.AppendingStringBuffer;


/**
 * Implementation of {@link HttpServletResponse} that saves the output in a string buffer. This is
 * used in REDIRECT_TO_BUFFER render strategy to create the buffer of the output that can be held on
 * to until the redirect part of the render strategy.
 * 
 * @author jcompagner
 */
class BufferedHttpServletResponse implements HttpServletResponse
{
	/** the print writer for the response */
	private StringBufferWriter sbw = new StringBufferWriter();
	private PrintWriter pw = new PrintWriter(sbw);

	/** cookies list */
	private List<Cookie> cookies;

	/** status code */
	private int status = -1;

	/** headers map */
	private MultiMap<String, Object> headers;

	/** the real response for encoding the url */
	private HttpServletResponse realResponse;

	private String redirect;
	private String contentType;
	private byte[] byteBuffer;
	private Locale locale;
	private String encoding;

	/**
	 * Constructor.
	 * 
	 * @param realResponse
	 *            The real response for encoding the url
	 */
	public BufferedHttpServletResponse(HttpServletResponse realResponse)
	{
		this.realResponse = realResponse;
	}

	/**
	 * @see javax.servlet.http.HttpServletResponse#addCookie(javax.servlet.http.Cookie)
	 */
	@Override
	public void addCookie(Cookie cookie)
	{
		isOpen();
		if (cookies == null)
		{
			cookies = new ArrayList<>(2);
		}
		cookies.add(cookie);
	}

	/**
	 * @see javax.servlet.http.HttpServletResponse#containsHeader(java.lang.String)
	 */
	@Override
	public boolean containsHeader(String name)
	{
		isOpen();
		if (headers == null)
		{
			return false;
		}
		return headers.containsKey(name);
	}

	/**
	 * @see javax.servlet.http.HttpServletResponse#encodeURL(java.lang.String)
	 */
	@Override
	public String encodeURL(String url)
	{
		isOpen();
		return realResponse.encodeURL(url);
	}

	/**
	 * @see javax.servlet.http.HttpServletResponse#encodeRedirectURL(java.lang.String)
	 */
	@Override
	public String encodeRedirectURL(String url)
	{
		isOpen();
		return realResponse.encodeRedirectURL(url);
	}

	/**
	 * @see javax.servlet.http.HttpServletResponse#encodeUrl(java.lang.String)
	 * @deprecated
	 */
	@Override
	@Deprecated
	public String encodeUrl(String url)
	{
		isOpen();
		return realResponse.encodeURL(url);
	}

	/**
	 * @see javax.servlet.http.HttpServletResponse#encodeRedirectUrl(java.lang.String)
	 * @deprecated
	 */
	@Override
	@Deprecated
	public String encodeRedirectUrl(String url)
	{
		isOpen();
		return realResponse.encodeRedirectURL(url);
	}

	/**
	 * @see javax.servlet.http.HttpServletResponse#sendError(int, java.lang.String)
	 */
	@Override
	public void sendError(int sc, String msg) throws IOException
	{
		isOpen();
		realResponse.sendError(sc, msg);
	}

	/**
	 * @see javax.servlet.http.HttpServletResponse#sendError(int)
	 */
	@Override
	public void sendError(int sc) throws IOException
	{
		isOpen();
		realResponse.sendError(sc);
	}

	/**
	 * @see javax.servlet.http.HttpServletResponse#sendRedirect(java.lang.String)
	 */
	@Override
	public void sendRedirect(String location) throws IOException
	{
		isOpen();
		redirect = location;
	}

	/**
	 * @return The redirect url
	 */
	public String getRedirectUrl()
	{
		isOpen();
		return redirect;
	}


	private void testAndCreateHeaders()
	{
		isOpen();
		if (headers == null)
		{
			headers = new MultiMap<>();
		}
	}

	private void isOpen()
	{
		if (realResponse == null)
		{
			throw new WicketRuntimeException("the buffered servlet response already closed.");
		}
	}

	/**
	 * @see javax.servlet.http.HttpServletResponse#setDateHeader(java.lang.String, long)
	 */
	@Override
	public void setDateHeader(String name, long date)
	{
		testAndCreateHeaders();
		headers.replaceValues(name, date);
	}

	/**
	 * @see javax.servlet.http.HttpServletResponse#addDateHeader(java.lang.String, long)
	 */
	@Override
	public void addDateHeader(String name, long date)
	{
		testAndCreateHeaders();
		headers.addValue(name, date);
	}

	/**
	 * @see javax.servlet.http.HttpServletResponse#setHeader(java.lang.String, java.lang.String)
	 */
	@Override
	public void setHeader(String name, String value)
	{
		testAndCreateHeaders();
		headers.replaceValues(name, value);
	}

	/**
	 * @see javax.servlet.http.HttpServletResponse#addHeader(java.lang.String, java.lang.String)
	 */
	@Override
	public void addHeader(String name, String value)
	{
		testAndCreateHeaders();
		headers.addValue(name, value);
	}

	/**
	 * @see javax.servlet.http.HttpServletResponse#setIntHeader(java.lang.String, int)
	 */
	@Override
	public void setIntHeader(String name, int value)
	{
		testAndCreateHeaders();
		headers.replaceValues(name, value);
	}

	/**
	 * @see javax.servlet.http.HttpServletResponse#addIntHeader(java.lang.String, int)
	 */
	@Override
	public void addIntHeader(String name, int value)
	{
		testAndCreateHeaders();
		headers.addValue(name, value);
	}

	/**
	 * @see javax.servlet.http.HttpServletResponse#setStatus(int)
	 */
	@Override
	public void setStatus(int statusCode)
	{
		status = statusCode;
	}

	/**
	 * @see javax.servlet.http.HttpServletResponse#setStatus(int, java.lang.String)
	 * @deprecated use setStatus(int) instead
	 */
	@Override
	@Deprecated
	public void setStatus(int sc, String sm)
	{
		setStatus(sc);
	}

	/**
	 * @see javax.servlet.ServletResponse#getCharacterEncoding()
	 */
	@Override
	public String getCharacterEncoding()
	{
		isOpen();
		return encoding;
	}

	/**
	 * Set the character encoding to use for the output.
	 * 
	 * @param encoding
	 */
	@Override
	public void setCharacterEncoding(String encoding)
	{
		this.encoding = encoding;
	}

	/**
	 * @see javax.servlet.ServletResponse#getOutputStream()
	 */
	@Override
	public ServletOutputStream getOutputStream() throws IOException
	{
		throw new UnsupportedOperationException("Cannot get output stream on BufferedResponse");
	}

	/**
	 * @see javax.servlet.ServletResponse#getWriter()
	 */
	@Override
	public PrintWriter getWriter() throws IOException
	{
		isOpen();
		return pw;
	}

	/**
	 * @see javax.servlet.ServletResponse#setContentLength(int)
	 */
	@Override
	public void setContentLength(int len)
	{
		isOpen();
		// ignored will be calculated when the buffer is really streamed.
	}

	@Override
	public void setContentLengthLong(long len)
	{
		isOpen();
		// ignored will be calculated when the buffer is really streamed.
	}

	/**
	 * @see javax.servlet.ServletResponse#setContentType(java.lang.String)
	 */
	@Override
	public void setContentType(String type)
	{
		isOpen();
		contentType = type;
	}

	/**
	 * @return The content type
	 */
	@Override
	public String getContentType()
	{
		return contentType;
	}

	/**
	 * @see javax.servlet.ServletResponse#setBufferSize(int)
	 */
	@Override
	public void setBufferSize(int size)
	{
		isOpen();
		// ignored every thing will be buffered
	}

	/**
	 * @see javax.servlet.ServletResponse#getBufferSize()
	 */
	@Override
	public int getBufferSize()
	{
		isOpen();
		return Integer.MAX_VALUE;
	}

	/**
	 * @see javax.servlet.ServletResponse#flushBuffer()
	 */
	@Override
	public void flushBuffer() throws IOException
	{
		isOpen();
	}

	/**
	 * @see javax.servlet.ServletResponse#resetBuffer()
	 */
	@Override
	public void resetBuffer()
	{
		isOpen();
		sbw.reset();
	}

	/**
	 * @see javax.servlet.ServletResponse#isCommitted()
	 */
	@Override
	public boolean isCommitted()
	{
		return pw == null;
	}

	/**
	 * @see javax.servlet.ServletResponse#reset()
	 */
	@Override
	public void reset()
	{
		resetBuffer();
		headers = null;
		cookies = null;
	}

	/**
	 * @see javax.servlet.ServletResponse#setLocale(java.util.Locale)
	 */
	@Override
	public void setLocale(Locale loc)
	{
		isOpen();
		locale = loc;
	}

	/**
	 * @see javax.servlet.ServletResponse#getLocale()
	 */
	@Override
	public Locale getLocale()
	{
		isOpen();
		if (locale == null)
		{
			return realResponse.getLocale();
		}
		return locale;
	}

	/**
	 * @return The length of the complete string buffer
	 */
	public int getContentLength()
	{
		isOpen();
		return sbw.getStringBuffer().length();
	}

	/**
	 * 
	 */
	public void close()
	{
		isOpen();
		pw.close();
		byteBuffer = convertToCharset(sbw.getStringBuffer(), getCharacterEncoding());

		pw = null;
		sbw = null;
		realResponse = null;
	}

	/**
	 * Convert the string into the output encoding required
	 * 
	 * @param output
	 * 
	 * @param encoding
	 *            The output encoding
	 * @return byte[] The encoded characters converted into bytes
	 */
	private static byte[] convertToCharset(final AppendingStringBuffer output, final String encoding)
	{
		if (encoding == null)
		{
			throw new WicketRuntimeException("Internal error: encoding must not be null");
		}

		final ByteArrayOutputStream baos = new ByteArrayOutputStream((int)(output.length() * 1.2));

		final OutputStreamWriter osw;
		final byte[] bytes;
		try
		{
			osw = new OutputStreamWriter(baos, encoding);
			osw.write(output.getValue(), 0, output.length());
			osw.close();

			bytes = baos.toByteArray();
		}
		catch (Exception ex)
		{
			throw new WicketRuntimeException("Can't convert response to charset: " + encoding, ex);
		}

		return bytes;
	}

	/**
	 * @param servletResponse
	 * @throws IOException
	 */
	public void writeTo(HttpServletResponse servletResponse) throws IOException
	{
		if (status != -1)
		{
			servletResponse.setStatus(status);
		}
		if (headers != null)
		{
			for (Entry<String, List<Object>> stringObjectEntry : headers.entrySet())
			{
				String name = stringObjectEntry.getKey();
				List<Object> values = stringObjectEntry.getValue();
				for (Object value : values)
				{
					addHeader(name, value, servletResponse);
				}
			}
		}

		if (cookies != null)
		{
			for (Cookie cookie : cookies)
			{
				servletResponse.addCookie(cookie);
			}
		}
		if (locale != null)
		{
			servletResponse.setLocale(locale);
		}
		// got a buffered response; now write it
		servletResponse.setContentLength(byteBuffer.length);
		servletResponse.setContentType(contentType);

		final OutputStream out = servletResponse.getOutputStream();
		out.write(byteBuffer);
		out.close();

	}

	/**
	 * @param name
	 *            Name of the header to set
	 * @param value
	 *            The value can be String/Long/Int
	 * @param servletResponse
	 *            The response to set it to.
	 */
	private static void setHeader(String name, Object value, HttpServletResponse servletResponse)
	{
		if (value instanceof String)
		{
			servletResponse.setHeader(name, (String)value);
		}
		else if (value instanceof Long)
		{
			servletResponse.setDateHeader(name, (Long)value);
		}
		else if (value instanceof Integer)
		{
			servletResponse.setIntHeader(name, (Integer)value);
		}
	}

	/**
	 * @param name
	 *            Name of the header to set
	 * @param value
	 *            The value can be String/Long/Int
	 * @param servletResponse
	 *            The response to set it to.
	 */
	private static void addHeader(String name, Object value, HttpServletResponse servletResponse)
	{
		if (value instanceof String)
		{
			servletResponse.addHeader(name, (String)value);
		}
		else if (value instanceof Long)
		{
			servletResponse.addDateHeader(name, (Long)value);
		}
		else if (value instanceof Integer)
		{
			servletResponse.addIntHeader(name, (Integer)value);
		}
	}

	/**
	 * @see javax.servlet.http.HttpServletResponse#setStatus(int)
	 * @return status
	 */
	@Override
	public int getStatus()
	{
		return status;
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getHeader(java.lang.String)
	 * @param name
	 * @return the first header with name
	 */
	@Override
	public String getHeader(String name)
	{
		Object value = headers.getFirstValue(name);
		if (value == null)
		{
			return null;
		}
		return value.toString();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getHeaders(java.lang.String)
	 * @param name
	 * @return all headers with name
	 */
	@Override
	public Collection<String> getHeaders(String name)
	{
		List<Object> values = headers.get(name);
		if (values == null)
		{
			return Collections.emptyList();
		}
		List<String> ret = new ArrayList<>(values.size());
		for (Object value : values)
		{
			ret.add(value.toString());
		}
		return ret;
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getHeaderNames()
	 * @return all header names
	 */
	@Override
	public Collection<String> getHeaderNames()
	{
		return Collections.unmodifiableCollection(headers.keySet());
	}
}
