/*
 * Copyright (C) 2017 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 art;

import org.apache.harmony.dalvik.ddmc.*;
import dalvik.system.VMDebug;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.function.*;
import java.util.zip.Adler32;
import java.nio.*;

public class Test1940 {
  public static final int DDMS_TYPE_INDEX = 0;
  public static final int DDMS_LEN_INDEX = 4;
  public static final int DDMS_HEADER_LENGTH = 8;
  public static final int MY_DDMS_TYPE = 0xDEADBEEF;
  public static final int MY_DDMS_RESPONSE_TYPE = 0xFADE7357;
  public static final int MY_EMPTY_DDMS_TYPE = 0xABCDEF01;
  public static final int MY_INVALID_DDMS_TYPE = 0x12345678;

  public static final boolean PRINT_ALL_CHUNKS = false;

  public static interface DdmHandler {
    public void HandleChunk(int type, byte[] data) throws Exception;
  }

  public static final class TestError extends Error {
    public TestError(String s) { super(s); }
  }

  private static void checkEq(Object a, Object b) {
    if (!a.equals(b)) {
      throw new TestError("Failure: " + a + " != " + b);
    }
  }

  private static boolean chunkEq(Chunk a, Chunk b) {
    return a.type == b.type &&
           a.offset == b.offset &&
           a.length == b.length &&
           Arrays.equals(a.data, b.data);
  }

  private static String printChunk(Chunk k) {
    byte[] out = new byte[k.length];
    System.arraycopy(k.data, k.offset, out, 0, k.length);
    return String.format("Chunk(Type: 0x%X, Len: %d, data: %s)",
        k.type, k.length, Arrays.toString(out));
  }

  private static final class MyDdmHandler extends ChunkHandler {
    public void onConnected() {}
    public void onDisconnected() {}
    public Chunk handleChunk(Chunk req) {
      System.out.println("MyDdmHandler: Chunk received: " + printChunk(req));
      if (req.type == MY_DDMS_TYPE) {
        // For this test we will simply calculate the checksum
        ByteBuffer b = ByteBuffer.wrap(new byte[8]);
        Adler32 a = new Adler32();
        a.update(req.data, req.offset, req.length);
        b.order(ByteOrder.BIG_ENDIAN);
        long val = a.getValue();
        b.putLong(val);
        System.out.printf("MyDdmHandler: Putting value 0x%X\n", val);
        Chunk ret = new Chunk(MY_DDMS_RESPONSE_TYPE, b.array(), 0, 8);
        System.out.println("MyDdmHandler: Chunk returned: " + printChunk(ret));
        return ret;
      } else if (req.type == MY_EMPTY_DDMS_TYPE) {
        return new Chunk(MY_DDMS_RESPONSE_TYPE, new byte[0], 0, 0);
      } else if (req.type == MY_INVALID_DDMS_TYPE) {
        // This is a very invalid chunk.
        return new Chunk(MY_DDMS_RESPONSE_TYPE, new byte[] { 0 }, /*offset*/ 12, /*length*/ 55);
      } else {
        throw new TestError("Unknown ddm request type: " + req.type);
      }
    }
  }

  public static final ChunkHandler SINGLE_HANDLER = new MyDdmHandler();

  public static DdmHandler CURRENT_HANDLER;

  public static void HandlePublish(int type, byte[] data) throws Exception {
    if (PRINT_ALL_CHUNKS) {
      System.out.println(
          "Unknown Chunk published: " + printChunk(new Chunk(type, data, 0, data.length)));
    }
    CURRENT_HANDLER.HandleChunk(type, data);
  }

  // TYPE Thread Create
  public static final int TYPE_THCR = 0x54484352;
  // Type Thread name
  public static final int TYPE_THNM = 0x54484E4D;
  // Type Thread death.
  public static final int TYPE_THDE = 0x54484445;
  // Type Heap info
  public static final int TYPE_HPIF = 0x48504946;
  // Type Trace Results
  public static final int TYPE_MPSE = 0x4D505345;

  public static boolean IsFromThread(Thread t, byte[] data) {
    // DDMS always puts the thread-id as the first 4 bytes.
    ByteBuffer b = ByteBuffer.wrap(data);
    b.order(ByteOrder.BIG_ENDIAN);
    return b.getInt() == (int) t.getId();
  }

  public static final class AwaitChunkHandler implements DdmHandler {
    public final Predicate<Chunk> needle;
    public final DdmHandler chain;
    private boolean found = false;
    public AwaitChunkHandler(Predicate<Chunk> needle, DdmHandler chain) {
      this.needle = needle;
      this.chain = chain;
    }
    public void HandleChunk(int type, byte[] data) throws Exception {
      chain.HandleChunk(type, data);
      Chunk c = new Chunk(type, data, 0, data.length);
      if (needle.test(c)) {
        synchronized (this) {
          found = true;
          notifyAll();
        }
      }
    }
    public synchronized void Await() throws Exception {
      while (!found) {
        wait();
      }
    }
  }

  public static void run() throws Exception {
    DdmHandler BaseHandler = (type, data) -> {
      System.out.println("Chunk published: " + printChunk(new Chunk(type, data, 0, data.length)));
    };
    CURRENT_HANDLER = BaseHandler;
    initializeTest();
    Method publish = Test1940.class.getDeclaredMethod("HandlePublish",
                                                      Integer.TYPE,
                                                      new byte[0].getClass());
    Thread listener = new Thread(() -> { Test1940.publishListen(publish); });
    listener.setDaemon(true);
    listener.start();
    // Test sending chunk directly.
    DdmServer.registerHandler(MY_DDMS_TYPE, SINGLE_HANDLER);
    DdmServer.registerHandler(MY_EMPTY_DDMS_TYPE, SINGLE_HANDLER);
    DdmServer.registerHandler(MY_INVALID_DDMS_TYPE, SINGLE_HANDLER);
    DdmServer.registrationComplete();
    byte[] data = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 };
    System.out.println("Sending data " + Arrays.toString(data));
    Chunk res = processChunk(data);
    System.out.println("JVMTI returned chunk: " + printChunk(res));

    // Test sending an empty chunk.
    System.out.println("Sending empty data array");
    res = processChunk(new byte[0]);
    System.out.println("JVMTI returned chunk: " + printChunk(res));

    // Test sending chunk through DdmServer#sendChunk
    Chunk c = new Chunk(
        MY_DDMS_TYPE, new byte[] { 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10 }, 0, 8);
    AwaitChunkHandler h = new AwaitChunkHandler((x) -> chunkEq(c, x), CURRENT_HANDLER);
    CURRENT_HANDLER = h;
    System.out.println("Sending chunk: " + printChunk(c));
    DdmServer.sendChunk(c);
    h.Await();
    CURRENT_HANDLER = BaseHandler;

    // Test getting back an empty chunk.
    data = new byte[] { 0x1 };
    System.out.println(
        "Sending data " + Arrays.toString(data) + " to chunk handler " + MY_EMPTY_DDMS_TYPE);
    res = processChunk(new Chunk(MY_EMPTY_DDMS_TYPE, data, 0, 1));
    System.out.println("JVMTI returned chunk: " + printChunk(res));

    // Test getting back an invalid chunk.
    System.out.println(
        "Sending data " + Arrays.toString(data) + " to chunk handler " + MY_INVALID_DDMS_TYPE);
    try {
      res = processChunk(new Chunk(MY_INVALID_DDMS_TYPE, data, 0, 1));
      System.out.println("JVMTI returned chunk: " + printChunk(res));
    } catch (RuntimeException e) {
      System.out.println("Got error: " + e.getMessage());
    }

    // Test thread chunks are sent.
    final boolean[] types_seen = new boolean[] { false, false, false };
    AwaitChunkHandler wait_thd= new AwaitChunkHandler(
      (x) -> types_seen[0] && types_seen[1] && types_seen[2],
      (type, cdata) -> {
        switch (type) {
          case TYPE_THCR:
            types_seen[0] = true;
            break;
          case TYPE_THNM:
            types_seen[1] = true;
            break;
          case TYPE_THDE:
            types_seen[2] = true;
            break;
          default:
            // We don't want to print other types.
            break;
        }
      });
    CURRENT_HANDLER = wait_thd;
    DdmVmInternal.setThreadNotifyEnabled(true);
    System.out.println("threadNotify started!");
    final Thread thr = new Thread(() -> { return; }, "THREAD");
    thr.start();
    System.out.println("Target thread started!");
    thr.join();
    System.out.println("Target thread finished!");
    DdmVmInternal.setThreadNotifyEnabled(false);
    System.out.println("threadNotify Disabled!");
    wait_thd.Await();
    // Make sure we saw at least one of Thread-create, Thread name, & thread death.
    if (!types_seen[0] || !types_seen[1] || !types_seen[2]) {
      System.out.println("Didn't see expected chunks for thread creation! got: " +
          Arrays.toString(types_seen));
    } else {
      System.out.println("Saw expected thread events.");
    }

    // method Tracing
    AwaitChunkHandler mpse = new AwaitChunkHandler((x) -> x.type == TYPE_MPSE, (type, cdata) -> {
      // This chunk includes timing and thread information so we just check the type.
      if (type == TYPE_MPSE) {
        System.out.println("Expected chunk type published: " + type);
      }
    });
    CURRENT_HANDLER = mpse;
    VMDebug.startMethodTracingDdms(/*size: default*/0,
                                   /*flags: none*/ 0,
                                   /*sampling*/ false,
                                   /*interval*/ 0);
    doNothing();
    doNothing();
    doNothing();
    doNothing();
    VMDebug.stopMethodTracing();
    mpse.Await();
  }

  private static void doNothing() {}
  private static Chunk processChunk(byte[] val) {
    return processChunk(new Chunk(MY_DDMS_TYPE, val, 0, val.length));
  }

  private static native void initializeTest();
  private static native Chunk processChunk(Chunk val);
  private static native void publishListen(Method publish);
}
