/*
 * 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.
 */

#include <inttypes.h>
#include <pthread.h>
#include <sched.h>

#include "android-base/logging.h"
#include "android-base/macros.h"
#include "jni.h"
#include "jvmti.h"
#include "scoped_local_ref.h"

// Test infrastructure
#include "jvmti_helper.h"
#include "test_env.h"

namespace art {
namespace Test930AgentThread {

struct AgentData {
  AgentData() : main_thread(nullptr),
                jvmti_env(nullptr),
                priority(0) {
  }

  jthread main_thread;
  jvmtiEnv* jvmti_env;
  pthread_barrier_t b;
  jint priority;
};

static void AgentMain(jvmtiEnv* jenv, JNIEnv* env, void* arg) {
  AgentData* data = reinterpret_cast<AgentData*>(arg);

  // Check some basics.
  // This thread is not the main thread.
  jthread this_thread;
  jvmtiError this_thread_result = jenv->GetCurrentThread(&this_thread);
  CheckJvmtiError(jenv, this_thread_result);
  CHECK(!env->IsSameObject(this_thread, data->main_thread));

  // The thread is a daemon.
  jvmtiThreadInfo info;
  jvmtiError info_result = jenv->GetThreadInfo(this_thread, &info);
  CheckJvmtiError(jenv, info_result);
  CHECK(info.is_daemon);
  CheckJvmtiError(jenv, jenv->Deallocate(reinterpret_cast<unsigned char*>(info.name)));
  if (info.thread_group != nullptr) {
    env->DeleteLocalRef(info.thread_group);
  }
  if (info.context_class_loader != nullptr) {
    env->DeleteLocalRef(info.context_class_loader);
  }

  // The thread has the requested priority.
  // TODO: Our thread priorities do not work on the host.
  // CHECK_EQ(info.priority, data->priority);

  // Check further parts of the thread:
  jint thread_count;
  jthread* threads;
  jvmtiError threads_result = jenv->GetAllThreads(&thread_count, &threads);
  CheckJvmtiError(jenv, threads_result);
  bool found = false;
  for (jint i = 0; i != thread_count; ++i) {
    if (env->IsSameObject(threads[i], this_thread)) {
      found = true;
      break;
    }
  }
  CHECK(found);

  // Done, let the main thread progress.
  int wait_result = pthread_barrier_wait(&data->b);
  CHECK(wait_result == PTHREAD_BARRIER_SERIAL_THREAD || wait_result == 0);
}

extern "C" JNIEXPORT void JNICALL Java_art_Test931_testAgentThread(
    JNIEnv* env, jclass Main_klass ATTRIBUTE_UNUSED) {
  // Create a Thread object.
  ScopedLocalRef<jobject> thread_name(env, env->NewStringUTF("Agent Thread"));
  if (thread_name.get() == nullptr) {
    return;
  }

  ScopedLocalRef<jclass> thread_klass(env, env->FindClass("java/lang/Thread"));
  if (thread_klass.get() == nullptr) {
    return;
  }
  ScopedLocalRef<jobject> thread(env, env->AllocObject(thread_klass.get()));
  if (thread.get() == nullptr) {
    return;
  }

  // Get a ThreadGroup from the current thread. We need a non-null one as we're gonna call a
  // runtime-only constructor (so we can set priority and daemon state).
  jvmtiThreadInfo cur_thread_info;
  jvmtiError info_result = jvmti_env->GetThreadInfo(nullptr, &cur_thread_info);
  if (JvmtiErrorToException(env, jvmti_env, info_result)) {
    return;
  }
  CheckJvmtiError(jvmti_env,
                  jvmti_env->Deallocate(reinterpret_cast<unsigned char*>(cur_thread_info.name)));
  ScopedLocalRef<jobject> thread_group(env, cur_thread_info.thread_group);
  if (cur_thread_info.context_class_loader != nullptr) {
    env->DeleteLocalRef(cur_thread_info.context_class_loader);
  }

  jmethodID initID = env->GetMethodID(thread_klass.get(),
                                      "<init>",
                                      "(Ljava/lang/ThreadGroup;Ljava/lang/String;IZ)V");
  if (initID == nullptr) {
    return;
  }
  env->CallNonvirtualVoidMethod(thread.get(),
                                thread_klass.get(),
                                initID,
                                thread_group.get(),
                                thread_name.get(),
                                0,
                                JNI_FALSE);
  if (env->ExceptionCheck()) {
    return;
  }

  jthread main_thread;
  jvmtiError main_thread_result = jvmti_env->GetCurrentThread(&main_thread);
  if (JvmtiErrorToException(env, jvmti_env, main_thread_result)) {
    return;
  }

  AgentData data;
  data.main_thread = env->NewGlobalRef(main_thread);
  data.jvmti_env = jvmti_env;
  data.priority = JVMTI_THREAD_MIN_PRIORITY;
  CHECK_EQ(0, pthread_barrier_init(&data.b, nullptr, 2));

  jvmtiError result = jvmti_env->RunAgentThread(thread.get(), AgentMain, &data, data.priority);
  if (JvmtiErrorToException(env, jvmti_env, result)) {
    return;
  }

  int wait_result = pthread_barrier_wait(&data.b);
  CHECK(wait_result == PTHREAD_BARRIER_SERIAL_THREAD || wait_result == 0);

  // Scheduling may mean that the agent thread is put to sleep. Wait until it's dead in an effort
  // to not unload the plugin and crash.
  for (;;) {
    sleep(1);
    jint thread_state;
    jvmtiError state_result = jvmti_env->GetThreadState(thread.get(), &thread_state);
    if (JvmtiErrorToException(env, jvmti_env, state_result)) {
      return;
    }
    if (thread_state == 0 ||                                    // Was never alive.
        (thread_state & JVMTI_THREAD_STATE_TERMINATED) != 0) {  // Was alive and died.
      break;
    }
  }
  // Yield and sleep a bit more, to give the plugin time to tear down the native thread structure.
  sched_yield();
  sleep(1);

  env->DeleteGlobalRef(data.main_thread);

  pthread_barrier_destroy(&data.b);
}

}  // namespace Test930AgentThread
}  // namespace art