//
//  ========================================================================
//  Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.servlets.gzip;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import javax.servlet.DispatcherType;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.ByteArrayOutputStream2;

/* ------------------------------------------------------------ */
/**
 * Skeletal implementation of a CompressedStream. This class adds compression features to a ServletOutputStream and takes care of setting response headers, etc.
 * Major work and configuration is done here. Subclasses using different kinds of compression only have to implement the abstract methods doCompress() and
 * setContentEncoding() using the desired compression and setting the appropriate Content-Encoding header string.
 */
public abstract class AbstractCompressedStream extends ServletOutputStream
{
    private final String _encoding;
    protected final String _vary;
    private final HttpServletRequest _request;
    protected final CompressedResponseWrapper _wrapper;
    protected final HttpServletResponse _response;
    protected OutputStream _out;
    protected ByteArrayOutputStream2 _bOut;
    protected OutputStream _compressedOutputStream;
    protected boolean _closed;
    protected boolean _doNotCompress;

    /**
     * Instantiates a new compressed stream.
     *
     */
    public AbstractCompressedStream(String encoding,HttpServletRequest request, CompressedResponseWrapper wrapper,String vary)
            throws IOException
    {
        _encoding=encoding;
        _request = request;
        _wrapper = wrapper;
        _response = (HttpServletResponse)wrapper.getResponse();
        _vary=vary;

        if (_wrapper.getMinCompressSize()==0)
            doCompress();
    }

    /* ------------------------------------------------------------ */
    /**
     * Reset buffer.
     */
    public void resetBuffer()
    {
        if (_response.isCommitted() || _compressedOutputStream!=null )
            throw new IllegalStateException("Committed");
        _closed = false;
        _out = null;
        _bOut = null;
        _doNotCompress = false;
    }

    /* ------------------------------------------------------------ */
    public void setBufferSize(int bufferSize)
    {
        if (_bOut!=null && _bOut.getBuf().length<bufferSize)
        {
            ByteArrayOutputStream2 b = new ByteArrayOutputStream2(bufferSize);
            b.write(_bOut.getBuf(),0,_bOut.size());
            _bOut=b;
        }
    }

    /* ------------------------------------------------------------ */
    public void setContentLength()
    {
        if (_doNotCompress)
        {
            long length=_wrapper.getContentLength();
            if (length>=0)
            {
                if (length < Integer.MAX_VALUE)
                    _response.setContentLength((int)length);
                else
                    _response.setHeader("Content-Length",Long.toString(length));
            }
        }
    }

    /* ------------------------------------------------------------ */
    /**
     * @see java.io.OutputStream#flush()
     */
    @Override
    public void flush() throws IOException
    {
        if (_out == null || _bOut != null)
        {
            long length=_wrapper.getContentLength();
            if (length > 0 && length < _wrapper.getMinCompressSize())
                doNotCompress(false);
            else
                doCompress();
        }

        _out.flush();
    }

    /* ------------------------------------------------------------ */
    /**
     * @see java.io.OutputStream#close()
     */
    @Override
    public void close() throws IOException
    {
        if (_closed)
            return;

        if (_wrapper.getRequest().getAttribute("javax.servlet.include.request_uri") != null)
            flush();
        else
        {
            if (_bOut != null)
            {
                long length=_wrapper.getContentLength();
                if (length < 0)
                {
                    length = _bOut.getCount();
                    _wrapper.setContentLength(length);
                }
                if (length < _wrapper.getMinCompressSize())
                    doNotCompress(false);
                else
                    doCompress();
            }
            else if (_out == null)
            {
                // No output
                doNotCompress(false);
            }

            if (_compressedOutputStream != null)
                _compressedOutputStream.close();
            else
                _out.close();
            _closed = true;
        }
    }

    /**
     * Finish.
     *
     * @throws IOException
     *             Signals that an I/O exception has occurred.
     */
    public void finish() throws IOException
    {
        if (!_closed)
        {
            if (_out == null || _bOut != null)
            {
                long length=_wrapper.getContentLength();
                if (length<0 &&_bOut==null || length >= 0 && length < _wrapper.getMinCompressSize())
                    doNotCompress(false);
                else
                    doCompress();
            }

            if (_compressedOutputStream != null && !_closed)
            {
                _closed = true;
                _compressedOutputStream.close();
            }
        }
    }

    /* ------------------------------------------------------------ */
    /**
     * @see java.io.OutputStream#write(int)
     */
    @Override
    public void write(int b) throws IOException
    {
        checkOut(1);
        _out.write(b);
    }

    /* ------------------------------------------------------------ */
    /**
     * @see java.io.OutputStream#write(byte[])
     */
    @Override
    public void write(byte b[]) throws IOException
    {
        checkOut(b.length);
        _out.write(b);
    }

    /* ------------------------------------------------------------ */
    /**
     * @see java.io.OutputStream#write(byte[], int, int)
     */
    @Override
    public void write(byte b[], int off, int len) throws IOException
    {
        checkOut(len);
        _out.write(b,off,len);
    }

    /**
     * Do compress.
     *
     * @throws IOException Signals that an I/O exception has occurred.
     */
    public void doCompress() throws IOException
    {
        if (_compressedOutputStream==null)
        {
            if (_response.isCommitted())
                throw new IllegalStateException();

            if (_encoding!=null)
            {
                String prefix = "";
                if (_request.getDispatcherType() == DispatcherType.INCLUDE)
                    prefix = Response.SET_INCLUDE_HEADER_PREFIX;
                setHeader(prefix+"Content-Encoding", _encoding);

                if (_response.containsHeader("Content-Encoding"))
                {
                    addHeader(prefix+"Vary",_vary);
                    _out=_compressedOutputStream=createStream();
                    if (_out!=null)
                    {
                        if (_bOut!=null)
                        {
                            _out.write(_bOut.getBuf(),0,_bOut.getCount());
                            _bOut=null;
                        }

                        String etag=_wrapper.getETag();
                        if (etag!=null)
                        {
                            int end = etag.length()-1;
                            if (etag.charAt(end)=='"')
                                setHeader(prefix+"ETag",etag.substring(0,end)+"--"+_encoding+'"');
                            else
                                setHeader(prefix+"ETag",etag+"--"+_encoding);
                        }
                        return;
                    }
                }
            }

            doNotCompress(true); // Send vary as it could have been compressed if encoding was present
        }
    }

    /**
     * Do not compress.
     *
     * @throws IOException
     *             Signals that an I/O exception has occurred.
     */
    public void doNotCompress(boolean sendVary) throws IOException
    {
        if (_compressedOutputStream != null)
            throw new IllegalStateException("Compressed output stream is already assigned.");
        if (_out == null || _bOut != null)
        {
            if (sendVary)
                addHeader("Vary",_vary);
            if (_wrapper.getETag()!=null)
                setHeader("ETag",_wrapper.getETag());

            _doNotCompress = true;

            _out = _response.getOutputStream();
            setContentLength();

            if (_bOut != null)
                _out.write(_bOut.getBuf(),0,_bOut.getCount());
            _bOut = null;
        }
    }

    /**
     * Check out.
     *
     * @param lengthToWrite
     *            the length
     * @throws IOException
     *             Signals that an I/O exception has occurred.
     */
    private void checkOut(int lengthToWrite) throws IOException
    {
        if (_closed)
            throw new IOException("CLOSED");

        if (_out == null)
        {
            // If this first write is larger than buffer size, then we are committing now
            if (lengthToWrite>_wrapper.getBufferSize())
            {
                // if we know this is all the content and it is less than minimum, then do not compress, otherwise do compress
                long length=_wrapper.getContentLength();
                if (length>=0 && length<_wrapper.getMinCompressSize())
                    doNotCompress(false);  // Not compressing by size, so no vary on request headers
                else
                    doCompress();
            }
            else
            {
                // start aggregating writes into a buffered output stream
                _out = _bOut = new ByteArrayOutputStream2(_wrapper.getBufferSize());
            }
        }
        // else are we aggregating writes?
        else if (_bOut !=null)
        {
            // We are aggregating into the buffered output stream.

            // If this write fills the buffer, then we are committing
            if (lengthToWrite>=(_bOut.getBuf().length - _bOut.getCount()))
            {
                // if we know this is all the content and it is less than minimum, then do not compress, otherwise do compress
                long length=_wrapper.getContentLength();
                if (length>=0 && length<_wrapper.getMinCompressSize())
                    doNotCompress(false);  // Not compressing by size, so no vary on request headers
                else
                    doCompress();
            }
        }
    }

    public OutputStream getOutputStream()
    {
        return _out;
    }

    public boolean isClosed()
    {
        return _closed;
    }

    /**
     * Allows derived implementations to replace PrintWriter implementation.
     */
    protected PrintWriter newWriter(OutputStream out, String encoding) throws UnsupportedEncodingException
    {
        return encoding == null?new PrintWriter(out):new PrintWriter(new OutputStreamWriter(out,encoding));
    }

    protected void addHeader(String name,String value)
    {
        _response.addHeader(name, value);
    }

    protected void setHeader(String name,String value)
    {
        _response.setHeader(name, value);
    }

    @Override
    public void setWriteListener(WriteListener writeListener)
    {
        throw new UnsupportedOperationException("Use AsyncGzipFilter");
    }


    @Override
    public boolean isReady()
    {
        throw new UnsupportedOperationException("Use AsyncGzipFilter");
    }

    /**
     * Create the stream fitting to the underlying compression type.
     *
     * @throws IOException
     *             Signals that an I/O exception has occurred.
     */
    protected abstract OutputStream createStream() throws IOException;


}
