/*
 * 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 compressionFilters;

import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.zip.GZIPOutputStream;

import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.WriteListener;

/**
 * Implementation of <b>ServletOutputStream</b> that works with
 * the CompressionServletResponseWrapper implementation.
 *
 * @author Amy Roh
 * @author Dmitri Valdin
 */
public class CompressionResponseStream extends ServletOutputStream {

    // ----------------------------------------------------------- Constructors

    /**
     * Construct a servlet output stream associated with the specified Response.
     *
     * @param responseWrapper The associated response wrapper
     * @param originalOutput the output stream
     */
    public CompressionResponseStream(
            CompressionServletResponseWrapper responseWrapper,
            ServletOutputStream originalOutput) {

        super();
        closed = false;
        this.response = responseWrapper;
        this.output = originalOutput;
    }


    // ----------------------------------------------------- Instance Variables


    /**
     * The threshold number which decides to compress or not.
     * Users can configure in web.xml to set it to fit their needs.
     */
    protected int compressionThreshold = 0;

    /**
     * The compression buffer size to avoid chunking
     */
    protected int compressionBuffer = 0;

    /**
     * The mime types to compress
     */
    protected String[] compressionMimeTypes = {"text/html", "text/xml", "text/plain"};

    /**
     * Debug level
     */
    private int debug = 0;

    /**
     * The buffer through which all of our output bytes are passed.
     */
    protected byte[] buffer = null;

    /**
     * The number of data bytes currently in the buffer.
     */
    protected int bufferCount = 0;

    /**
     * The underlying gzip output stream to which we should write data.
     */
    protected OutputStream gzipstream = null;

    /**
     * Has this stream been closed?
     */
    protected boolean closed = false;

    /**
     * The response with which this servlet output stream is associated.
     */
    protected final CompressionServletResponseWrapper response;

    /**
     * The underlying servlet output stream to which we should write data.
     */
    protected final ServletOutputStream output;


    // --------------------------------------------------------- Public Methods

    /**
     * Set debug level.
     *
     * @param debug The higher the number, the more detail shown. Currently the
     *              range 0 (none) to 3 (everything) is used.
     */
    public void setDebugLevel(int debug) {
        this.debug = debug;
    }


    /**
     * Set the compressionThreshold number and create buffer for this size
     *
     * @param compressionThreshold Responses above this size in bytes will be
     *                             compressed
     */
    protected void setCompressionThreshold(int compressionThreshold) {
        this.compressionThreshold = compressionThreshold;
        buffer = new byte[this.compressionThreshold];
        if (debug > 1) {
            System.out.println("compressionThreshold is set to "+ this.compressionThreshold);
        }
    }

    /**
     * The compression buffer size to avoid chunking
     *
     * @param compressionBuffer The compression buffer size in bytes
     */
    protected void setCompressionBuffer(int compressionBuffer) {
        this.compressionBuffer = compressionBuffer;
        if (debug > 1) {
            System.out.println("compressionBuffer is set to "+ this.compressionBuffer);
        }
    }

    /**
     * Set supported mime types.
     *
     * @param compressionMimeTypes The mimetypes that will be compressed.
     */
    public void setCompressionMimeTypes(String[] compressionMimeTypes) {
        this.compressionMimeTypes = compressionMimeTypes;
        if (debug > 1) {
            System.out.println("compressionMimeTypes is set to " +
                    Arrays.toString(this.compressionMimeTypes));
        }
    }

    /**
     * Close this output stream, causing any buffered data to be flushed and
     * any further output data to throw an IOException.
     */
    @Override
    public void close() throws IOException {

        if (debug > 1) {
            System.out.println("close() @ CompressionResponseStream");
        }
        if (closed) {
            throw new IOException("This output stream has already been closed");
        }

        if (gzipstream != null) {
            flushToGZip();
            gzipstream.close();
            gzipstream = null;
        } else {
            if (bufferCount > 0) {
                if (debug > 2) {
                    System.out.print("output.write(");
                    System.out.write(buffer, 0, bufferCount);
                    System.out.println(")");
                }
                output.write(buffer, 0, bufferCount);
                bufferCount = 0;
            }
        }

        output.close();
        closed = true;

    }


    /**
     * Flush any buffered data for this output stream, which also causes the
     * response to be committed.
     */
    @Override
    public void flush() throws IOException {

        if (debug > 1) {
            System.out.println("flush() @ CompressionResponseStream");
        }
        if (closed) {
            throw new IOException("Cannot flush a closed output stream");
        }

        if (gzipstream != null) {
            gzipstream.flush();
        }

    }

    public void flushToGZip() throws IOException {

        if (debug > 1) {
            System.out.println("flushToGZip() @ CompressionResponseStream");
        }
        if (bufferCount > 0) {
            if (debug > 1) {
                System.out.println("flushing out to GZipStream, bufferCount = " + bufferCount);
            }
            writeToGZip(buffer, 0, bufferCount);
            bufferCount = 0;
        }

    }

    /**
     * Write the specified byte to our output stream.
     *
     * @param b The byte to be written
     *
     * @exception IOException if an input/output error occurs
     */
    @Override
    public void write(int b) throws IOException {

        if (debug > 1) {
            System.out.println("write "+b+" in CompressionResponseStream ");
        }
        if (closed) {
            throw new IOException("Cannot write to a closed output stream");
        }

        if (bufferCount >= buffer.length) {
            flushToGZip();
        }

        buffer[bufferCount++] = (byte) b;

    }


    /**
     * Write <code>b.length</code> bytes from the specified byte array
     * to our output stream.
     *
     * @param b The byte array to be written
     *
     * @exception IOException if an input/output error occurs
     */
    @Override
    public void write(byte b[]) throws IOException {

        write(b, 0, b.length);

    }



    /**
     * TODO SERVLET 3.1
     */
    @Override
    public boolean isReady() {
        // TODO Auto-generated method stub
        return false;
    }


    /**
     * TODO SERVLET 3.1
     */
    @Override
    public void setWriteListener(WriteListener listener) {
        // TODO Auto-generated method stub

    }


    /**
     * Write <code>len</code> bytes from the specified byte array, starting
     * at the specified offset, to our output stream.
     *
     * @param b The byte array containing the bytes to be written
     * @param off Zero-relative starting offset of the bytes to be written
     * @param len The number of bytes to be written
     *
     * @exception IOException if an input/output error occurs
     */
    @Override
    public void write(byte b[], int off, int len) throws IOException {

        if (debug > 1) {
            System.out.println("write, bufferCount = " + bufferCount + " len = " + len + " off = " + off);
        }
        if (debug > 2) {
            System.out.print("write(");
            System.out.write(b, off, len);
            System.out.println(")");
        }

        if (closed) {
            throw new IOException("Cannot write to a closed output stream");
        }

        if (len == 0) {
            return;
        }

        // Can we write into buffer ?
        if (len <= (buffer.length - bufferCount)) {
            System.arraycopy(b, off, buffer, bufferCount, len);
            bufferCount += len;
            return;
        }

        // There is not enough space in buffer. Flush it ...
        flushToGZip();

        // ... and try again. Note, that bufferCount = 0 here !
        if (len <= (buffer.length - bufferCount)) {
            System.arraycopy(b, off, buffer, bufferCount, len);
            bufferCount += len;
            return;
        }

        // write direct to gzip
        writeToGZip(b, off, len);
    }

    public void writeToGZip(byte b[], int off, int len) throws IOException {

        if (debug > 1) {
            System.out.println("writeToGZip, len = " + len);
        }
        if (debug > 2) {
            System.out.print("writeToGZip(");
            System.out.write(b, off, len);
            System.out.println(")");
        }
        if (gzipstream == null) {
            if (debug > 1) {
                System.out.println("new GZIPOutputStream");
            }

            boolean alreadyCompressed = false;
            String contentEncoding = response.getHeader("Content-Encoding");
            if (contentEncoding != null) {
                if (contentEncoding.contains("gzip")) {
                    alreadyCompressed = true;
                    if (debug > 0) {
                        System.out.println("content is already compressed");
                    }
                } else {
                    if (debug > 0) {
                        System.out.println("content is not compressed yet");
                    }
                }
            }

            boolean compressibleMimeType = false;
            // Check for compatible MIME-TYPE
            if (compressionMimeTypes != null) {
                if (startsWithStringArray(compressionMimeTypes, response.getContentType())) {
                    compressibleMimeType = true;
                    if (debug > 0) {
                        System.out.println("mime type " + response.getContentType() + " is compressible");
                    }
                } else {
                    if (debug > 0) {
                        System.out.println("mime type " + response.getContentType() + " is not compressible");
                    }
                }
            }

            if (response.isCommitted()) {
                if (debug > 1) {
                    System.out.print("Response already committed. Using original output stream");
                }
                gzipstream = output;
            } else if (alreadyCompressed) {
                if (debug > 1) {
                    System.out.print("Response already compressed. Using original output stream");
                }
                gzipstream = output;
            } else if (!compressibleMimeType) {
                if (debug > 1) {
                    System.out.print("Response mime type is not compressible. Using original output stream");
                }
                gzipstream = output;
            } else {
                response.addHeader("Content-Encoding", "gzip");
                response.setContentLength(-1);  // don't use any preset content-length as it will be wrong after gzipping
                response.setBufferSize(compressionBuffer);
                gzipstream = new GZIPOutputStream(output);
            }
        }
        gzipstream.write(b, off, len);

    }


    // -------------------------------------------------------- Package Methods

    /**
     * Has this response stream been closed?
     *
     * @return <code>true</code> if the stream has been closed, otherwise false.
     */
    public boolean closed() {
        return closed;
    }


    /**
     * Checks if any entry in the string array starts with the specified value
     *
     * @param sArray the StringArray
     * @param value string
     */
    private boolean startsWithStringArray(String sArray[], String value) {
        if (value == null) {
            return false;
        }
        for (String s : sArray) {
            if (value.startsWith(s)) {
                return true;
            }
        }
        return false;
    }
}
