//  Copyright 2004 The Apache Software Foundation
//
// Licensed 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.tapestry.request;

import java.io.IOException;
import java.io.OutputStream;
import java.net.SocketException;

import javax.servlet.http.HttpServletResponse;

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

/**
 *  A special output stream works with a {@link HttpServletResponse}, buffering
 *  data so as to defer opening the response's output stream.
 *
 *  <p>The buffering is pretty simple because the code
 *  between {@link org.apache.tapestry.IMarkupWriter} and this shows lots of buffering
 *  after the <code>PrintWriter</code> and inside the <code>OutputStreamWriter</code> that
 *  can't be configured.
 *
 *  <p>This class performs some buffering, but it is not all that
 *  useful because the 
 *  {@link org.apache.tapestry.html.Body} component (which will
 *  be used on virtually all Tapestry pages), buffers its wrapped contents
 *  (that is, evertyhing inside the &lt;body&gt; tag in the generated HTML).
 *
 *  @author Howard Lewis Ship
 *  @version $Id: ResponseOutputStream.java 243791 2004-02-19 17:38:13Z hlship $
 * 
 **/

public class ResponseOutputStream extends OutputStream
{
    private static final Log LOG = LogFactory.getLog(ResponseOutputStream.class);

    /**
     *  Default size for the buffer (2000 bytes).
     *
     **/

    public static final int DEFAULT_SIZE = 2000;

    private int _pos;
    private int _maxSize;
    private byte[] _buffer;

    private String _contentType;
    private HttpServletResponse _response;
    private OutputStream _out;

    private boolean _discard = false;

    /**
     *  Creates the stream with the default maximum buffer size.
     *
     **/

    public ResponseOutputStream(HttpServletResponse response)
    {
        this(response, DEFAULT_SIZE);
    }

    /**
     *  Standard constructor.
     *
     **/

    public ResponseOutputStream(HttpServletResponse response, int maxSize)
    {
        _response = response;
        _maxSize = maxSize;
    }

    /**
     *  Does nothing.  This is because of chaining of <code>close()</code> from
     *  {@link org.apache.tapestry.IMarkupWriter#close()} ... see {@link #flush()}.
     * 
     **/

    public void close() throws IOException
    {
        // Does nothing.
    }

    /**
     *  Flushes the underlying output stream, if is has been opened.  
     *
     *  <p>This method explicitly <em>does not</em> flush the internal buffer ...
     *  that's because when an {@link org.apache.tapestry.IMarkupWriter} is closed (for instance, because
     *  an exception is thrown), that <code>close()</code> spawns <code>flush()</code>es
     *  and <code>close()</code>s throughout the output stream chain, eventually
     *  reaching this method.
     *
     *  @see #forceFlush()
     *
     **/

    public void flush() throws IOException
    {
        try
        {
            if (_out != null)
                _out.flush();
        }
        catch (SocketException ex)
        {
            LOG.debug("Socket exception.");
        }
    }

    /**
     *  Writes the internal buffer to the output stream, opening it if necessary, then
     *  flushes the output stream.  Future writes will go directly to the output stream.
     *
     **/

    public void forceFlush() throws IOException
    {
        if (_out == null)
        {

            // In certain cases (such as when the Tapestry service sends a redirect),
            // there is no output to send back (and no content type set).  In this
            // case, forceFlush() does nothing.

            if (_buffer == null)
                return;

            open();
        }

        try
        {
            _out.flush();
        }
        catch (SocketException ex)
        {
            LOG.debug("Socket exception.");
        }
    }

    public String getContentType()
    {
        return _contentType;
    }

    public boolean getDiscard()
    {
        return _discard;
    }

    /**
     *  Sets the response type to from the contentType property (which
     *  defaults to "text/html") and gets an output stream
     *  from the response, then writes the current buffer to it and
     *  releases the buffer.
     *
     *  @throws IOException if the content type has never been set.
     *
     **/

    private void open() throws IOException
    {
        if (_contentType == null)
            throw new IOException(Tapestry.getMessage("ResponseOutputStream.content-type-not-set"));

        _response.setContentType(_contentType);

        _out = _response.getOutputStream();

        innerWrite(_buffer, 0, _pos);

        _pos = 0;
        _buffer = null;
    }

    /**
     *  Discards all output in the buffer.  This is used after an error to
     *  restart the output (so that the error may be presented).
     *
     *  <p>Clears the discard flag.
     *
     **/

    public void reset() throws IOException
    {
        _pos = 0;
        _discard = false;
    }

    /**
     *  Changes the maximum buffer size.  If the new buffer size is smaller
     *  than the number of
     *  bytes already in the buffer, the buffer is immediately flushed.
     *
     **/

    public void setBufferSize(int value) throws IOException
    {
        if (value < _pos)
        {
            open();
            return;
        }

        _maxSize = value;
    }

    public void setContentType(String value)
    {
        _contentType = value;
    }

    /**
     *  Indicates whether the stream should ignore all data written to it.
     *
     **/

    public void setDiscard(boolean value)
    {
        _discard = value;
    }

    private void innerWrite(byte[] b, int off, int len) throws IOException
    {
        if (b == null || len == 0 || _discard)
            return;

        try
        {
            _out.write(b, off, len);
        }
        catch (SocketException ex)
        {
            LOG.debug("Socket exception.");
        }
    }

    public void write(byte b[], int off, int len) throws IOException
    {
        if (len == 0 || _discard)
            return;

        if (_out != null)
        {
            _out.write(b, off, len);
            return;
        }

        // If too large for the maximum size buffer, then open the output stream
        // write out and free the buffer, and write out the new stuff.

        if (_pos + len >= _maxSize)
        {
            open();
            innerWrite(b, off, len);
            return;
        }

        // Allocate the buffer when it is initially needed.

        if (_buffer == null)
            _buffer = new byte[_maxSize];

        // Copy the new bytes into the buffer and advance the position.

        System.arraycopy(b, off, _buffer, _pos, len);
        _pos += len;
    }

    public void write(int b) throws IOException
    {
        if (_discard)
            return;

        // This method is rarely called so this little inefficiency is better than
        // maintaining that ugly buffer expansion code in two places.

        byte[] tiny = new byte[] {(byte) b };

        write(tiny, 0, 1);
    }
}