/* //device/tools/ddms/libs/ddmlib/src/com/android/ddmlib/JdwpPacket.java
**
** Copyright 2007, The Android Open Source Project
**
** 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 com.android.ddmlib;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.SocketChannel;

/**
 * A JDWP packet, sitting at the start of a ByteBuffer somewhere.
 *
 * This allows us to wrap a "pointer" to the data with the results of
 * decoding the packet.
 *
 * None of the operations here are synchronized.  If multiple threads will
 * be accessing the same ByteBuffers, external sync will be required.
 *
 * Use the constructor to create an empty packet, or "findPacket()" to
 * wrap a JdwpPacket around existing data.
 */
final class JdwpPacket {
    // header len
    public static final int JDWP_HEADER_LEN = 11;

    // results from findHandshake
    public static final int HANDSHAKE_GOOD = 1;
    public static final int HANDSHAKE_NOTYET = 2;
    public static final int HANDSHAKE_BAD = 3;

    // our cmdSet/cmd
    private static final int DDMS_CMD_SET = 0xc7;       // 'G' + 128
    private static final int DDMS_CMD = 0x01;

    // "flags" field
    private static final int REPLY_PACKET = 0x80;

    // this is sent and expected at the start of a JDWP connection
    private static final byte[] mHandshake = {
        'J', 'D', 'W', 'P', '-', 'H', 'a', 'n', 'd', 's', 'h', 'a', 'k', 'e'
    };

    public static final int HANDSHAKE_LEN = mHandshake.length;

    private ByteBuffer mBuffer;
    private int mLength, mId, mFlags, mCmdSet, mCmd, mErrCode;
    private boolean mIsNew;

    private static int sSerialId = 0x40000000;


    /**
     * Create a new, empty packet, in "buf".
     */
    JdwpPacket(ByteBuffer buf) {
        mBuffer = buf;
        mIsNew = true;
    }

    /**
     * Finish a packet created with newPacket().
     *
     * This always creates a command packet, with the next serial number
     * in sequence.
     *
     * We have to take "payloadLength" as an argument because we can't
     * see the position in the "slice" returned by getPayload().  We could
     * fish it out of the chunk header, but it's legal for there to be
     * more than one chunk in a JDWP packet.
     *
     * On exit, "position" points to the end of the data.
     */
    void finishPacket(int payloadLength) {
        assert mIsNew;

        ByteOrder oldOrder = mBuffer.order();
        mBuffer.order(ChunkHandler.CHUNK_ORDER);

        mLength = JDWP_HEADER_LEN + payloadLength;
        mId = getNextSerial();
        mFlags = 0;
        mCmdSet = DDMS_CMD_SET;
        mCmd = DDMS_CMD;

        mBuffer.putInt(0x00, mLength);
        mBuffer.putInt(0x04, mId);
        mBuffer.put(0x08, (byte) mFlags);
        mBuffer.put(0x09, (byte) mCmdSet);
        mBuffer.put(0x0a, (byte) mCmd);

        mBuffer.order(oldOrder);
        mBuffer.position(mLength);
    }

    /**
     * Get the next serial number.  This creates a unique serial number
     * across all connections, not just for the current connection.  This
     * is a useful property when debugging, but isn't necessary.
     *
     * We can't synchronize on an int, so we use a sync method.
     */
    private static synchronized int getNextSerial() {
        return sSerialId++;
    }

    /**
     * Return a slice of the byte buffer, positioned past the JDWP header
     * to the start of the chunk header.  The buffer's limit will be set
     * to the size of the payload if the size is known; if this is a
     * packet under construction the limit will be set to the end of the
     * buffer.
     *
     * Doesn't examine the packet at all -- works on empty buffers.
     */
    ByteBuffer getPayload() {
        ByteBuffer buf;
        int oldPosn = mBuffer.position();

        mBuffer.position(JDWP_HEADER_LEN);
        buf = mBuffer.slice();     // goes from position to limit
        mBuffer.position(oldPosn);

        if (mLength > 0)
            buf.limit(mLength - JDWP_HEADER_LEN);
        else
            assert mIsNew;
        buf.order(ChunkHandler.CHUNK_ORDER);
        return buf;
    }

    /**
     * Returns "true" if this JDWP packet has a JDWP command type.
     *
     * This never returns "true" for reply packets.
     */
    boolean isDdmPacket() {
        return (mFlags & REPLY_PACKET) == 0 &&
               mCmdSet == DDMS_CMD_SET &&
               mCmd == DDMS_CMD;
    }

    /**
     * Returns "true" if this JDWP packet is tagged as a reply.
     */
    boolean isReply() {
        return (mFlags & REPLY_PACKET) != 0;
    }

    /**
     * Returns "true" if this JDWP packet is a reply with a nonzero
     * error code.
     */
    boolean isError() {
        return isReply() && mErrCode != 0;
    }

    /**
     * Returns "true" if this JDWP packet has no data.
     */
    boolean isEmpty() {
        return (mLength == JDWP_HEADER_LEN);
    }

    /**
     * Return the packet's ID.  For a reply packet, this allows us to
     * match the reply with the original request.
     */
    int getId() {
        return mId;
    }

    /**
     * Return the length of a packet.  This includes the header, so an
     * empty packet is 11 bytes long.
     */
    int getLength() {
        return mLength;
    }

    /**
     * Write our packet to "chan".  Consumes the packet as part of the
     * write.
     *
     * The JDWP packet starts at offset 0 and ends at mBuffer.position().
     */
    void writeAndConsume(SocketChannel chan) throws IOException {
        int oldLimit;

        //Log.i("ddms", "writeAndConsume: pos=" + mBuffer.position()
        //    + ", limit=" + mBuffer.limit());

        assert mLength > 0;

        mBuffer.flip();         // limit<-posn, posn<-0
        oldLimit = mBuffer.limit();
        mBuffer.limit(mLength);
        while (mBuffer.position() != mBuffer.limit()) {
            chan.write(mBuffer);
        }
        // position should now be at end of packet
        assert mBuffer.position() == mLength;

        mBuffer.limit(oldLimit);
        mBuffer.compact();      // shift posn...limit, posn<-pending data

        //Log.i("ddms", "               : pos=" + mBuffer.position()
        //    + ", limit=" + mBuffer.limit());
    }

    /**
     * "Move" the packet data out of the buffer we're sitting on and into
     * buf at the current position.
     */
    void movePacket(ByteBuffer buf) {
        Log.v("ddms", "moving " + mLength + " bytes");
        int oldPosn = mBuffer.position();

        mBuffer.position(0);
        mBuffer.limit(mLength);
        buf.put(mBuffer);
        mBuffer.position(mLength);
        mBuffer.limit(oldPosn);
        mBuffer.compact();      // shift posn...limit, posn<-pending data
    }

    /**
     * Consume the JDWP packet.
     *
     * On entry and exit, "position" is the #of bytes in the buffer.
     */
    void consume()
    {
        //Log.d("ddms", "consuming " + mLength + " bytes");
        //Log.d("ddms", "  posn=" + mBuffer.position()
        //    + ", limit=" + mBuffer.limit());

        /*
         * The "flip" call sets "limit" equal to the position (usually the
         * end of data) and "position" equal to zero.
         *
         * compact() copies everything from "position" and "limit" to the
         * start of the buffer, sets "position" to the end of data, and
         * sets "limit" to the capacity.
         *
         * On entry, "position" is set to the amount of data in the buffer
         * and "limit" is set to the capacity.  We want to call flip()
         * so that position..limit spans our data, advance "position" past
         * the current packet, then compact.
         */
        mBuffer.flip();         // limit<-posn, posn<-0
        mBuffer.position(mLength);
        mBuffer.compact();      // shift posn...limit, posn<-pending data
        mLength = 0;
        //Log.d("ddms", "  after compact, posn=" + mBuffer.position()
        //    + ", limit=" + mBuffer.limit());
    }

    /**
     * Find the JDWP packet at the start of "buf".  The start is known,
     * but the length has to be parsed out.
     *
     * On entry, the packet data in "buf" must start at offset 0 and end
     * at "position".  "limit" should be set to the buffer capacity.  This
     * method does not alter "buf"s attributes.
     *
     * Returns a new JdwpPacket if a full one is found in the buffer.  If
     * not, returns null.  Throws an exception if the data doesn't look like
     * a valid JDWP packet.
     */
    static JdwpPacket findPacket(ByteBuffer buf) {
        int count = buf.position();
        int length, id, flags, cmdSet, cmd;

        if (count < JDWP_HEADER_LEN)
            return null;

        ByteOrder oldOrder = buf.order();
        buf.order(ChunkHandler.CHUNK_ORDER);

        length = buf.getInt(0x00);
        id = buf.getInt(0x04);
        flags = buf.get(0x08) & 0xff;
        cmdSet = buf.get(0x09) & 0xff;
        cmd = buf.get(0x0a) & 0xff;

        buf.order(oldOrder);

        if (length < JDWP_HEADER_LEN)
            throw new BadPacketException();
        if (count < length)
            return null;

        JdwpPacket pkt = new JdwpPacket(buf);
        //pkt.mBuffer = buf;
        pkt.mLength = length;
        pkt.mId = id;
        pkt.mFlags = flags;

        if ((flags & REPLY_PACKET) == 0) {
            pkt.mCmdSet = cmdSet;
            pkt.mCmd = cmd;
            pkt.mErrCode = -1;
        } else {
            pkt.mCmdSet = -1;
            pkt.mCmd = -1;
            pkt.mErrCode = cmdSet | (cmd << 8);
        }

        return pkt;
    }

    /**
     * Like findPacket(), but when we're expecting the JDWP handshake.
     *
     * Returns one of:
     *   HANDSHAKE_GOOD   - found handshake, looks good
     *   HANDSHAKE_BAD    - found enough data, but it's wrong
     *   HANDSHAKE_NOTYET - not enough data has been read yet
     */
    static int findHandshake(ByteBuffer buf) {
        int count = buf.position();
        int i;

        if (count < mHandshake.length)
            return HANDSHAKE_NOTYET;

        for (i = mHandshake.length -1; i >= 0; --i) {
            if (buf.get(i) != mHandshake[i])
                return HANDSHAKE_BAD;
        }

        return HANDSHAKE_GOOD;
    }

    /**
     * Remove the handshake string from the buffer.
     *
     * On entry and exit, "position" is the #of bytes in the buffer.
     */
    static void consumeHandshake(ByteBuffer buf) {
        // in theory, nothing else can have arrived, so this is overkill
        buf.flip();         // limit<-posn, posn<-0
        buf.position(mHandshake.length);
        buf.compact();      // shift posn...limit, posn<-pending data
    }

    /**
     * Copy the handshake string into the output buffer.
     *
     * On exit, "buf"s position will be advanced.
     */
    static void putHandshake(ByteBuffer buf) {
        buf.put(mHandshake);
    }
}

