/*
 * Copyright (C) 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 com.android.annotations.NonNull;
import com.android.ddmlib.DebugPortManager.IDebugPortProvider;
import com.android.ddmlib.jdwp.JdwpAgent;
import com.android.ddmlib.jdwp.JdwpInterceptor;

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

/**
 * Subclass this with a class that handles one or more chunk types.
 */
abstract class ChunkHandler extends JdwpInterceptor {

    public static final int CHUNK_HEADER_LEN = 8;   // 4-byte type, 4-byte len
    public static final ByteOrder CHUNK_ORDER = ByteOrder.BIG_ENDIAN;

    public static final int CHUNK_FAIL = type("FAIL");

    public static final int DDMS_CMD_SET = 0xc7;       // 'G' + 128

    public static final int DDMS_CMD = 0x01;

    ChunkHandler() {}

    /**
     * Client is ready.  The monitor thread calls this method on all
     * handlers when the client is determined to be DDM-aware (usually
     * after receiving a HELO response.)
     *
     * The handler can use this opportunity to initialize client-side
     * activity.  Because there's a fair chance we'll want to send a
     * message to the client, this method can throw an IOException.
     */
    abstract void clientReady(Client client) throws IOException;

    /**
     * Client has gone away.  Can be used to clean up any resources
     * associated with this client connection.
     */
    abstract void clientDisconnected(Client client);

    /**
     * Handle an incoming chunk.  The data, of chunk type "type", begins
     * at the start of "data" and continues to data.limit().
     *
     * If "isReply" is set, then "msgId" will be the ID of the request
     * we sent to the client.  Otherwise, it's the ID generated by the
     * client for this event.  Note that it's possible to receive chunks
     * in reply packets for which we are not registered.
     *
     * The handler may not modify the contents of "data".
     */
    abstract void handleChunk(Client client, int type,
        ByteBuffer data, boolean isReply, int msgId);

    /**
     * Handle chunks not recognized by handlers.  The handleChunk() method
     * in sub-classes should call this if the chunk type isn't recognized.
     */
    protected void handleUnknownChunk(Client client, int type,
        ByteBuffer data, boolean isReply, int msgId) {
        if (type == CHUNK_FAIL) {
            int errorCode, msgLen;
            String msg;

            errorCode = data.getInt();
            msgLen = data.getInt();
            msg = ByteBufferUtil.getString(data, msgLen);
            Log.w("ddms", "WARNING: failure code=" + errorCode + " msg=" + msg);
        } else {
            Log.w("ddms", "WARNING: received unknown chunk " + name(type)
                + ": len=" + data.limit() + ", reply=" + isReply
                + ", msgId=0x" + Integer.toHexString(msgId));
        }
        Log.w("ddms", "         client " + client + ", handler " + this);
    }

    /**
     * Utility function to copy a String out of a ByteBuffer.
     */
    public static String getString(ByteBuffer buf, int len) {
      return ByteBufferUtil.getString(buf, len);
    }

    /**
     * Convert a 4-character string to a 32-bit type.
     */
    static int type(String typeName) {
        int val = 0;

        if (typeName.length() != 4) {
            Log.e("ddms", "Type name must be 4 letter long");
            throw new RuntimeException("Type name must be 4 letter long");
        }

        for (int i = 0; i < 4; i++) {
            val <<= 8;
            val |= (byte) typeName.charAt(i);
        }

        return val;
    }

    /**
     * Convert an integer type to a 4-character string.
     */
    static String name(int type) {
        char[] ascii = new char[4];

        ascii[0] = (char) ((type >> 24) & 0xff);
        ascii[1] = (char) ((type >> 16) & 0xff);
        ascii[2] = (char) ((type >> 8) & 0xff);
        ascii[3] = (char) (type & 0xff);

        return new String(ascii);
    }

    /**
     * Allocate a ByteBuffer with enough space to hold the JDWP packet
     * header and one chunk header in addition to the demands of the
     * chunk being created.
     *
     * "maxChunkLen" indicates the size of the chunk contents only.
     */
    static ByteBuffer allocBuffer(int maxChunkLen) {
        ByteBuffer buf =
            ByteBuffer.allocate(JdwpPacket.JDWP_HEADER_LEN + 8 +maxChunkLen);
        buf.order(CHUNK_ORDER);
        return buf;
    }

    /**
     * Return the slice of the JDWP packet buffer that holds just the
     * chunk data.
     */
    static ByteBuffer getChunkDataBuf(ByteBuffer jdwpBuf) {
        ByteBuffer slice;

        assert jdwpBuf.position() == 0;

        jdwpBuf.position(JdwpPacket.JDWP_HEADER_LEN + CHUNK_HEADER_LEN);
        slice = jdwpBuf.slice();
        slice.order(CHUNK_ORDER);
        jdwpBuf.position(0);

        return slice;
    }

    /**
     * Write the chunk header at the start of the chunk.
     *
     * Pass in the byte buffer returned by JdwpPacket.getPayload().
     */
    static void finishChunkPacket(JdwpPacket packet, int type, int chunkLen) {
        ByteBuffer buf = packet.getPayload();

        buf.putInt(0x00, type);
        buf.putInt(0x04, chunkLen);

        packet.finishPacket(DDMS_CMD_SET, DDMS_CMD, CHUNK_HEADER_LEN + chunkLen);
    }

    /**
     * Check that the client is opened with the proper debugger port for the
     * specified application name, and if not, reopen it.
     * @param client
     * @param appName
     * @return
     */
    protected static Client checkDebuggerPortForAppName(Client client, String appName) {
        IDebugPortProvider provider = DebugPortManager.getProvider();
        if (provider != null) {
            Device device = client.getDeviceImpl();
            int newPort = provider.getPort(device, appName);

            if (newPort != IDebugPortProvider.NO_STATIC_PORT &&
                    newPort != client.getDebuggerListenPort()) {

                AndroidDebugBridge bridge = AndroidDebugBridge.getBridge();
                if (bridge != null) {
                    DeviceMonitor deviceMonitor = bridge.getDeviceMonitor();
                    if (deviceMonitor != null) {
                        deviceMonitor.addClientToDropAndReopen(client, newPort);
                        client = null;
                    }
                }
            }
        }

        return client;
    }

    void handlePacket(Client client, JdwpPacket packet) {
        ByteBuffer buf = packet.getPayload();
        int type = buf.getInt();
        int length = buf.getInt();
        Log.d("ddms", "Calling handler for " + name(type)
                + " [" + this + "] (len=" + length + ")");
        ByteBuffer ibuf = buf.slice();
        ByteBuffer roBuf = ibuf.asReadOnlyBuffer(); // enforce R/O
        roBuf.order(CHUNK_ORDER);

        handleChunk(client, type, roBuf, packet.isReply(), packet.getId());
    }

    @Override
    public JdwpPacket intercept(@NonNull JdwpAgent agent, @NonNull JdwpPacket packet) {
      // TODO: ChunkHandlers are specific to client only packages. Further refactoring
      // is needed to properly generalize them to JdwpInterceptors
      if (agent instanceof Client) {
        Client client = (Client)agent;
        // TODO: ChunkHandlers are currently all static objects created in static
        // initializers. For many different reasons they should not be there and should
        // be moved to another creation mechanism where they are part of the ddm extension
        // workflow. For now, access the ddmextension directly.
        MonitorThread.getInstance().getDdmExtension().ddmSeen(client);

        if (packet.isError()) {
          client.packetFailed(packet);
        } else if (packet.isEmpty()) {
          Log.d("ddms", "Got empty reply for 0x" + Integer.toHexString(packet.getId()));
        } else {
          handlePacket(client, packet);
        }
        return null;
      }
      return packet;
    }
}

