// Copyright 2009, 2012 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.tapestry5.http.internal.gzip;

import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.zip.GZIPOutputStream;

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

import org.apache.tapestry5.http.internal.TapestryHttpInternalConstants;
import org.apache.tapestry5.http.services.CompressionAnalyzer;

/**
 * A buffered output stream that, when a certain number of bytes is buffered (the cutover point) will open a compressed
 * stream (via {@link org.apache.tapestry5.http.services.Response#getOutputStream(String)}
 */
public class BufferedGZipOutputStream extends ServletOutputStream
{
    private final String contentType;

    private final HttpServletResponse response;

    private final CompressionAnalyzer analyzer;

    private final int cutover;

    private ByteArrayOutputStream byteArrayOutputStream;

    /**
     * Initially the ByteArrayOutputStream, later the response output stream (possibly wrapped with a
     * GZIPOutputStream).
     */
    private OutputStream currentOutputStream;

    public BufferedGZipOutputStream(String contentType, HttpServletResponse response, int cutover,
                                    CompressionAnalyzer analyzer)
    {
        this.contentType = contentType;
        this.response = response;
        this.cutover = cutover;
        this.analyzer = analyzer;

        byteArrayOutputStream = new ByteArrayOutputStream(cutover);

        currentOutputStream = byteArrayOutputStream;
    }

    private void checkForCutover() throws IOException
    {
        if (byteArrayOutputStream == null) return;

        if (byteArrayOutputStream.size() < cutover) return;

        // Time to switch over to GZIP.
        openResponseOutputStream(true);
    }

    private void openResponseOutputStream(boolean gzip) throws IOException
    {
        OutputStream responseOutputStream = response.getOutputStream();

        boolean useCompression = gzip && analyzer.isCompressable(contentType);

        OutputStream possiblyCompressed = useCompression
                ? new GZIPOutputStream(responseOutputStream)
                : responseOutputStream;

        if (useCompression)
        {
            response.setHeader(
                    TapestryHttpInternalConstants.CONTENT_ENCODING_HEADER, 
                    TapestryHttpInternalConstants.GZIP_CONTENT_ENCODING);
        }

        currentOutputStream =
                new BufferedOutputStream(possiblyCompressed);

        // Write what content we already have to the new stream.

        byteArrayOutputStream.writeTo(currentOutputStream);

        byteArrayOutputStream = null;
    }

    public void write(int b) throws IOException
    {
        currentOutputStream.write(b);

        checkForCutover();
    }

    @Override
    public void write(byte[] b) throws IOException
    {
        currentOutputStream.write(b);

        checkForCutover();
    }

    @Override
    public void write(byte[] b, int off, int len) throws IOException
    {
        currentOutputStream.write(b, off, len);

        checkForCutover();
    }

    @Override
    public void flush() throws IOException
    {
        forceOutputStream().flush();
    }

    @Override
    public void close() throws IOException
    {
        // When closing, if we haven't accumulated enough output yet to start compressing,
        // then send what we have, uncompressed.

        forceOutputStream().close();
    }

    private OutputStream forceOutputStream() throws IOException
    {
        if (byteArrayOutputStream != null)
            openResponseOutputStream(false);

        return currentOutputStream;
    }
}
