/*
 * 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.request.resource;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import org.apache.commons.io.input.BoundedInputStream;
import org.apache.wicket.protocol.http.servlet.ResponseIOException;
import org.apache.wicket.request.resource.AbstractResource.WriteCallback;
import org.apache.wicket.request.resource.IResource.Attributes;
import org.apache.wicket.util.io.IOUtils;
import org.apache.wicket.util.io.Streams;
import org.apache.wicket.util.lang.Args;

/**
 * Used to read a part of an input stream and writes it to the output stream of the response taken
 * from attributes in {@link #writeData(org.apache.wicket.request.resource.IResource.Attributes)}
 * method.
 *
 * @author Tobias Soloschenko
 * @since 7.0.0
 */
public class PartWriterCallback extends WriteCallback
{
	/**
	 * The input stream to read from
	 */
	private final InputStream inputStream;

	/**
	 * The total length to read if {@link #endbyte} is not specified
	 */
	private final Long contentLength;

	/**
	 * The byte to start reading from. If omitted then the input stream will be read from its
	 * beginning
	 */
	private Long startbyte;

	/**
	 * The end byte to read from the {@link #inputStream}. If omitted then the input stream will be
	 * read till its end
	 */
	private Long endbyte;

	/**
	 * The size of the buffer that is used for the copying of the data
	 */
	private int bufferSize;

	/**
	 * If the given input stream is going to be closed
	 */
	private boolean close = false;


	/**
	 * Creates a part writer callback.<br>
	 * <br>
	 * Reads a part of the given input stream. If the startbyte parameter is not null the number of
	 * bytes are skipped till the stream is read. If the endbyte is not null the stream is read till
	 * endbyte, else to the end of the whole stream. If startbyte and endbyte is null the whole
	 * stream is copied.
	 *
	 * @param inputStream
	 *            the input stream to read from
	 * @param contentLength
	 *            content length of the input stream. Ignored if <em>endByte</em> is specified
	 * @param startbyte
	 *            the start position to read from (if not null the number of bytes are skipped till
	 *            the stream is read)
	 * @param endbyte
	 *            the end position to read to (if not null the stream is going to be read till
	 *            endbyte, else to the end of the whole stream)
	 */
	public PartWriterCallback(InputStream inputStream, Long contentLength, Long startbyte,
		Long endbyte)
	{
		this.inputStream = inputStream;
		this.contentLength = Args.notNull(contentLength, "contentLength");
		this.startbyte = startbyte;
		this.endbyte = endbyte;
	}

	/**
	 * Writes the data
	 *
	 * @param attributes
	 *            the attributes to get the output stream of the response
	 * @throws IOException
	 *             if something went wrong while writing the data to the output stream
	 */
	@Override
	public void writeData(Attributes attributes) throws IOException
	{
		try
		{
			OutputStream outputStream = attributes.getResponse().getOutputStream();
			byte[] buffer = new byte[getBufferSize()];

			if (startbyte != null || endbyte != null)
			{
				// skipping the first bytes which are
				// requested to be skipped by the client
				if (startbyte != null)
				{
					inputStream.skip(startbyte);
				}
				else
				{
					// If no start byte has been given set it to 0
					// which means no bytes has been skipped
					startbyte = 0L;
				}

				// If there are no end bytes given read the whole stream till the end
				if (endbyte == null || Long.valueOf(-1).equals(endbyte))
				{
					endbyte = contentLength;
				}

				BoundedInputStream boundedInputStream = null;
				try
				{
					// Stream is going to be read from the starting point next to the skipped bytes
					// till the end byte computed by the range between startbyte / endbyte
					boundedInputStream = new BoundedInputStream(inputStream,
						(endbyte - startbyte) + 1);

					// The original input stream is going to be closed by the end of the request
					// so set propagate close to false
					boundedInputStream.setPropagateClose(false);

					// The read bytes in the current buffer
					int readBytes;

					while ((readBytes = boundedInputStream.read(buffer)) != -1)
					{
						outputStream.write(buffer, 0, readBytes);
					}
				}
				finally
				{
					IOUtils.closeQuietly(boundedInputStream);
				}
			}
			else
			{
				// No range has been given so copy the content
				// from input stream to the output stream
				Streams.copy(inputStream, outputStream, getBufferSize());
			}
		}
		catch (ResponseIOException e)
		{
			// the client has closed the connection and
			// doesn't read the stream further on
			// (in tomcats
			// org.apache.catalina.connector.ClientAbortException)
			// we ignore this case
		}
		if (close)
		{
			IOUtils.close(inputStream);
		}
	}

	/**
	 * Sets the buffer size used to send the data to the client
	 *
	 * @return the buffer size used to send the data to the client (default is 4096)
	 */
	public int getBufferSize()
	{
		return bufferSize > 0 ? bufferSize : 4096;
	}

	/**
	 * Sets the buffer size used to send the data to the client
	 *
	 * @param bufferSize
	 *            the buffer size used to send the data to the client
	 * @return the part writer callback
	 */
	public PartWriterCallback setBufferSize(int bufferSize)
	{
		this.bufferSize = bufferSize;
		return this;
	}

	/**
	 * If the given input stream is going to be closed
	 * 
	 * @return if the given input stream is going to be closed
	 */
	public boolean isClose()
	{
		return close;
	}

	/**
	 * If set true the given input stream is going to be closed
	 * 
	 * @param close
	 *            if the given input stream is going to be closed
	 * @return the part writer callback
	 */
	public PartWriterCallback setClose(boolean close)
	{
		this.close = close;
		return this;

	}

}
