//========================================================================
//Copyright 2006 Mort Bay Consulting Pty. Ltd.
//------------------------------------------------------------------------
//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.mortbay.jetty.ajp;

import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;

import org.mortbay.io.Buffer;
import org.mortbay.io.Buffers;
import org.mortbay.io.ByteArrayBuffer;
import org.mortbay.io.EndPoint;
import org.mortbay.jetty.AbstractGenerator;
import org.mortbay.jetty.EofException;
import org.mortbay.jetty.HttpFields;
import org.mortbay.jetty.HttpVersions;
import org.mortbay.jetty.HttpFields.Field;
import org.mortbay.log.Log;
import org.mortbay.util.TypeUtil;

/**
 * @author lagdeppa (at) exist.com
 * @author Greg Wilkins
 */
public class Ajp13Generator extends AbstractGenerator
{
    private static HashMap __headerHash = new HashMap();

    static
    {
        byte[] xA001 =
        { (byte) 0xA0, (byte) 0x01 };
        byte[] xA002 =
        { (byte) 0xA0, (byte) 0x02 };
        byte[] xA003 =
        { (byte) 0xA0, (byte) 0x03 };
        byte[] xA004 =
        { (byte) 0xA0, (byte) 0x04 };
        byte[] xA005 =
        { (byte) 0xA0, (byte) 0x05 };
        byte[] xA006 =
        { (byte) 0xA0, (byte) 0x06 };
        byte[] xA007 =
        { (byte) 0xA0, (byte) 0x07 };
        byte[] xA008 =
        { (byte) 0xA0, (byte) 0x08 };
        byte[] xA009 =
        { (byte) 0xA0, (byte) 0x09 };
        byte[] xA00A =
        { (byte) 0xA0, (byte) 0x0A };
        byte[] xA00B =
        { (byte) 0xA0, (byte) 0x0B };
        __headerHash.put("Content-Type", xA001);
        __headerHash.put("Content-Language", xA002);
        __headerHash.put("Content-Length", xA003);
        __headerHash.put("Date", xA004);
        __headerHash.put("Last-Modified", xA005);
        __headerHash.put("Location", xA006);
        __headerHash.put("Set-Cookie", xA007);
        __headerHash.put("Set-Cookie2", xA008);
        __headerHash.put("Servlet-Engine", xA009);
        __headerHash.put("Status", xA00A);
        __headerHash.put("WWW-Authenticate", xA00B);

    }

    private static final byte[] AJP13_END_RESPONSE =
    { 'A', 'B', 0, 2, 5, 1 };

    // AB ajp respose
    // 0, 3 int = 3 packets in length
    // 6, send signal to get more data
    // 31, -7 byte values for int 8185 = (8 * 1024) - 7 MAX_DATA
    private static final byte[] AJP13_MORE_CONTENT =
    { 'A', 'B', 0, 3, 6, 31, -7 };

    private static String SERVER = "Server: Jetty(6.0.x)";

    public static void setServerVersion(String version)
    {
        SERVER = "Jetty(" + version + ")";
    }

    /* ------------------------------------------------------------ */
    private boolean _expectMore = false;

    private boolean _needMore = false;

    private boolean _needEOC = false;

    private boolean _bufferPrepared = false;

    /* ------------------------------------------------------------ */
    public Ajp13Generator(Buffers buffers, EndPoint io, int headerBufferSize, int contentBufferSize)
    {
        super(buffers, io, headerBufferSize, contentBufferSize);
    }

    /* ------------------------------------------------------------ */
    public void reset(boolean returnBuffers)
    {
        super.reset(returnBuffers);
        _needEOC = false;
        _needMore = false;
        _expectMore = false;
        _bufferPrepared = false;
    }

    /* ------------------------------------------------------------ */
    /**
     * Add content.
     * 
     * @param content
     * @param last
     * @throws IllegalArgumentException
     *             if <code>content</code> is
     *             {@link Buffer#isImmutable immutable}.
     * @throws IllegalStateException
     *             If the request is not expecting any more content, or if the
     *             buffers are full and cannot be flushed.
     * @throws IOException
     *             if there is a problem flushing the buffers.
     */
    public void addContent(Buffer content, boolean last) throws IOException
    {

        if (_noContent)
        {
            content.clear();
            return;
        }

        if (content.isImmutable())
            throw new IllegalArgumentException("immutable");

        if (_last || _state == STATE_END)
        {
            Log.debug("Ignoring extra content {}", content);
            content.clear();
            return;
        }
        _last = last;

        // Handle any unfinished business?
        if (_content != null && _content.length() > 0)
        {

            flush();
            if (_content != null && _content.length() > 0)
                throw new IllegalStateException("FULL");
        }

        _content = content;

        _contentWritten += content.length();

        // Handle the _content
        if (_head)
        {

            content.clear();
            _content = null;
        }
        else
        {
            // Yes - so we better check we have a buffer
            initContent();
            // Copy _content to buffer;
            int len = 0;
            len = _buffer.put(_content);

            // make sure there is space for a trailing null
            if (len > 0 && _buffer.space() == 0)
            {
                len--;
                _buffer.setPutIndex(_buffer.putIndex() - 1);
            }

            _content.skip(len);

            if (_content.length() == 0)
                _content = null;
        }
    }

    /* ------------------------------------------------------------ */
    /**
     * Add content.
     * 
     * @param b
     *            byte
     * @return true if the buffers are full
     * @throws IOException
     */
    public boolean addContent(byte b) throws IOException
    {
        if (_noContent)
            return false;

        if (_last || _state == STATE_END)
            throw new IllegalStateException("Closed");

        // Handle any unfinished business?
        if (_content != null && _content.length() > 0)
        {
            flush();
            if (_content != null && _content.length() > 0)
                throw new IllegalStateException("FULL");
        }

        _contentWritten++;

        // Handle the _content
        if (_head)
            return false;

        // we better check we have a buffer
        initContent();

        // Copy _content to buffer;

        _buffer.put(b);

        return _buffer.space() <= 1;
    }

    /* ------------------------------------------------------------ */
    /**
     * Prepare buffer for unchecked writes. Prepare the generator buffer to
     * receive unchecked writes
     * 
     * @return the available space in the buffer.
     * @throws IOException
     */
    protected int prepareUncheckedAddContent() throws IOException
    {
        if (_noContent)
            return -1;

        if (_last || _state == STATE_END)
            throw new IllegalStateException("Closed");

        // Handle any unfinished business?
        Buffer content = _content;
        if (content != null && content.length() > 0)
        {
            flush();
            if (content != null && content.length() > 0)
                throw new IllegalStateException("FULL");
        }

        // we better check we have a buffer
        initContent();

        _contentWritten -= _buffer.length();

        // Handle the _content
        if (_head)
            return Integer.MAX_VALUE;

        return _buffer.space() - 1;
    }

    /* ------------------------------------------------------------ */
    public void completeHeader(HttpFields fields, boolean allContentAdded) throws IOException
    {
        if (_state != STATE_HEADER)
            return;

        if (_last && !allContentAdded)
            throw new IllegalStateException("last?");
        _last = _last | allContentAdded;

        boolean has_server = false;
        if (_version == HttpVersions.HTTP_1_0_ORDINAL)
            _close = true;

        // get a header buffer
        if (_header == null)
            _header = _buffers.getBuffer(_headerBufferSize);

        Buffer tmpbuf = _buffer;
        _buffer = _header;

        try
        {
            // start the header
            _buffer.put((byte) 'A');
            _buffer.put((byte) 'B');
            addInt(0);
            _buffer.put((byte) 0x4);
            addInt(_status);
            if (_reason == null)
                _reason = getReasonBuffer(_status);
            if (_reason == null)
                _reason = new ByteArrayBuffer(TypeUtil.toString(_status));
            addBuffer(_reason);

            if (_status == 100 || _status == 204 || _status == 304)
            {
                _noContent = true;
                _content = null;
            }

            // allocate 2 bytes for number of headers
            int field_index = _buffer.putIndex();
            addInt(0);

            // Add headers
            Iterator i = fields.getFields();
            int num_fields = 0;

            while (i.hasNext())
            {
                num_fields++;
                Field f = (Field) i.next();

                byte[] codes = (byte[]) __headerHash.get(f.getName());
                if (codes != null)
                {
                    _buffer.put(codes);
                }
                else
                {
                    addString(f.getName());
                }
                addString(f.getValue());
            }

            if (!has_server && _status > 100 && getSendServerVersion())
            {
                num_fields++;
                addString("Server");
                addString(SERVER);
            }

            // TODO Add content length if last content known.

            // insert the number of headers
            int tmp = _buffer.putIndex();
            _buffer.setPutIndex(field_index);
            addInt(num_fields);
            _buffer.setPutIndex(tmp);

            // get the payload size ( - 4 bytes for the ajp header)
            // excluding the
            // ajp header
            int payloadSize = _buffer.length() - 4;
            // insert the total packet size on 2nd and 3rd byte that
            // was previously
            // allocated
            addInt(2, payloadSize);
        }
        finally
        {
            _buffer = tmpbuf;
        }

        _state = STATE_CONTENT;

    }

    /* ------------------------------------------------------------ */
    /**
     * Complete the message.
     * 
     * @throws IOException
     */
    public void complete() throws IOException
    {
        if (_state == STATE_END)
            return;

        super.complete();

        if (_state < STATE_FLUSHING)
        {
            _state = STATE_FLUSHING;
            _needEOC = true;
        }

        flush();
    }

    /* ------------------------------------------------------------ */
    public long flush() throws IOException
    {
        try
        {
            if (!_expectMore && _state == STATE_HEADER)
                throw new IllegalStateException("State==HEADER");

            prepareBuffers();

            if (_endp == null)
            {
                // TODO - probably still needed!
                // if (_needMore && _buffer != null)
                // {
                // if(!_hasSentEOC)
                // _buffer.put(AJP13_MORE_CONTENT);
                // }
                if (!_expectMore && _needEOC && _buffer != null)
                {
                    _buffer.put(AJP13_END_RESPONSE);
                }
                _needEOC = false;
                return 0;
            }

            // Keep flushing while there is something to flush
            // (except break below)
            int total = 0;
            long last_len = -1;
            Flushing: while (true)
            {
                int len = -1;
                int to_flush = ((_header != null && _header.length() > 0) ? 4 : 0) | ((_buffer != null && _buffer.length() > 0) ? 2 : 0);

                switch (to_flush)
                {
                case 7:
                    throw new IllegalStateException(); // should
                    // never
                    // happen!
                case 6:
                    len = _endp.flush(_header, _buffer, null);

                    break;
                case 5:
                    throw new IllegalStateException(); // should
                    // never
                    // happen!
                case 4:
                    len = _endp.flush(_header);
                    break;
                case 3:
                    throw new IllegalStateException(); // should
                    // never
                    // happen!
                case 2:
                    len = _endp.flush(_buffer);

                    break;
                case 1:
                    throw new IllegalStateException(); // should
                    // never
                    // happen!
                case 0:
                {
                    // Nothing more we can write now.
                    if (_header != null)
                        _header.clear();

                    _bufferPrepared = false;

                    if (_buffer != null)
                    {
                        _buffer.clear();

                        // reserve some space for the
                        // header
                        _buffer.setPutIndex(7);
                        _buffer.setGetIndex(7);

                        // Special case handling for
                        // small left over buffer from
                        // an addContent that caused a
                        // buffer flush.
                        if (_content != null && _content.length() < _buffer.space() && _state != STATE_FLUSHING)
                        {

                            _buffer.put(_content);
                            _content.clear();
                            _content = null;
                            break Flushing;
                        }

                    }

                    // Are we completely finished for now?
                    if (!_expectMore && !_needEOC && (_content == null || _content.length() == 0))
                    {
                        if (_state == STATE_FLUSHING)
                            _state = STATE_END;
                        if (_state == STATE_END/* &&_close */)
                            _endp.close();

                        break Flushing;
                    }

                    // Try to prepare more to write.
                    prepareBuffers();
                }
                }

                // If we failed to flush anything twice in a row
                // break
                if (len <= 0)
                {
                    if (last_len <= 0)
                        break Flushing;
                    break;
                }
                last_len = len;
                total += len;
            }

            return total;
        }
        catch (IOException e)
        {
            Log.ignore(e);
            throw (e instanceof EofException) ? e : new EofException(e);
        }

    }

    /* ------------------------------------------------------------ */
    private void prepareBuffers()
    {
        if (!_bufferPrepared)
        {
            // Refill buffer if possible
            if (_content != null && _content.length() > 0 && _buffer != null && _buffer.space() > 0)
            {

                int len = _buffer.put(_content);

                // Make sure there is space for a trailing null
                if (len > 0 && _buffer.space() == 0)
                {
                    len--;
                    _buffer.setPutIndex(_buffer.putIndex() - 1);
                }
                _content.skip(len);

                if (_content.length() == 0)
                    _content = null;

                if (_buffer.length() == 0)
                {
                    _content = null;
                }
            }

            // add header if needed
            if (_buffer != null)
            {

                int payloadSize = _buffer.length();

                // 4 bytes for the ajp header
                // 1 byte for response type
                // 2 bytes for the response size
                // 1 byte because we count from zero??

                if (payloadSize > 0)
                {
                    _bufferPrepared = true;

                    _buffer.put((byte) 0);
                    int put = _buffer.putIndex();
                    _buffer.setGetIndex(0);
                    _buffer.setPutIndex(0);
                    _buffer.put((byte) 'A');
                    _buffer.put((byte) 'B');
                    addInt(payloadSize + 4);
                    _buffer.put((byte) 3);
                    addInt(payloadSize);
                    _buffer.setPutIndex(put);
                }
            }

            if (_needMore)
            {

                if (_header == null)
                {
                    _header = _buffers.getBuffer(_headerBufferSize);
                }

                if (_buffer == null && _header != null && _header.space() >= AJP13_MORE_CONTENT.length)
                {
                    _header.put(AJP13_MORE_CONTENT);
                    _needMore = false;
                }
                else if (_buffer != null && _buffer.space() >= AJP13_MORE_CONTENT.length)
                {
                    // send closing packet if all contents
                    // are added
                    _buffer.put(AJP13_MORE_CONTENT);
                    _needMore = false;
                    _bufferPrepared = true;
                }

            }

            if (!_expectMore && _needEOC)
            {
                if (_buffer == null && _header.space() >= AJP13_END_RESPONSE.length)
                {

                    _header.put(AJP13_END_RESPONSE);
                    _needEOC = false;
                }
                else if (_buffer != null && _buffer.space() >= AJP13_END_RESPONSE.length)
                {
                    // send closing packet if all contents
                    // are added

                    _buffer.put(AJP13_END_RESPONSE);
                    _needEOC = false;
                    _bufferPrepared = true;
                }
            }
        }
    }

    /* ------------------------------------------------------------ */
    public boolean isComplete()
    {
        return !_expectMore && _state == STATE_END;
    }

    /* ------------------------------------------------------------ */
    private void initContent() throws IOException
    {
        if (_buffer == null)
        {
            _buffer = _buffers.getBuffer(_contentBufferSize);
            _buffer.setPutIndex(7);
            _buffer.setGetIndex(7);
        }
    }

    /* ------------------------------------------------------------ */
    private void addInt(int i)
    {
        _buffer.put((byte) ((i >> 8) & 0xFF));
        _buffer.put((byte) (i & 0xFF));
    }

    /* ------------------------------------------------------------ */
    private void addInt(int startIndex, int i)
    {
        _buffer.poke(startIndex, (byte) ((i >> 8) & 0xFF));
        _buffer.poke((startIndex + 1), (byte) (i & 0xFF));
    }

    /* ------------------------------------------------------------ */
    private void addString(String str)
    {
        if (str == null)
        {
            addInt(0xFFFF);
            return;
        }

        // TODO - need to use a writer to convert, to avoid this hacky
        // conversion and temp buffer
        byte[] b = str.getBytes();

        addInt(b.length);

        _buffer.put(b);
        _buffer.put((byte) 0);
    }

    /* ------------------------------------------------------------ */
    private void addBuffer(Buffer b)
    {
        if (b == null)
        {
            addInt(0xFFFF);
            return;
        }

        addInt(b.length());
        _buffer.put(b);
        _buffer.put((byte) 0);
    }

    /* ------------------------------------------------------------ */
    public boolean isNeedMore()
    {
        // TODO - this is not correct. See setNeedMore
        return _expectMore;
    }

    /* ------------------------------------------------------------ */
    public void setNeedMore(boolean needMore) throws IOException
    {
        _needMore = needMore;
        if (needMore)
        {
            _expectMore = true;
            flush();
        }
    }

    public void setExpectMore(boolean expectMore)
    {
        _expectMore = expectMore;
    }

}
