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

#include <stack>
#include <string>
#include <unordered_map>
#include <vector>

#include "android-base/logging.h"
#include "android-base/macros.h"
#include "jni_binder.h"
#include "jni_helper.h"
#include "jvmti_helper.h"
#include "jvmti.h"
#include "scoped_primitive_array.h"
#include "test_env.h"

namespace art {

extern "C" JNIEXPORT jint JNICALL Java_android_jvmti_cts_JvmtiRedefineClassesTest_redefineClass(
    JNIEnv* env, jclass klass ATTRIBUTE_UNUSED, jclass target, jbyteArray dex_bytes) {
  jvmtiClassDefinition def;
  def.klass = target;
  def.class_byte_count = static_cast<jint>(env->GetArrayLength(dex_bytes));
  signed char* redef_bytes = env->GetByteArrayElements(dex_bytes, nullptr);
  jvmtiError res =jvmti_env->Allocate(def.class_byte_count,
                                      const_cast<unsigned char**>(&def.class_bytes));
  if (res != JVMTI_ERROR_NONE) {
    return static_cast<jint>(res);
  }
  memcpy(const_cast<unsigned char*>(def.class_bytes), redef_bytes, def.class_byte_count);
  env->ReleaseByteArrayElements(dex_bytes, redef_bytes, 0);
  // Do the redefinition.
  res = jvmti_env->RedefineClasses(1, &def);
  return static_cast<jint>(res);
}

extern "C" JNIEXPORT jint JNICALL Java_android_jvmti_cts_JvmtiRedefineClassesTest_retransformClass(
    JNIEnv* env ATTRIBUTE_UNUSED, jclass klass ATTRIBUTE_UNUSED, jclass target) {
  return jvmti_env->RetransformClasses(1, &target);
}

class TransformationData {
 public:
  TransformationData() : redefinitions_(), should_pop_(false) {}

  void SetPop(bool val) {
    should_pop_ = val;
  }

  void ClearRedefinitions() {
    redefinitions_.clear();
  }

  void PushRedefinition(std::string name, std::vector<unsigned char> data) {
    if (redefinitions_.find(name) == redefinitions_.end()) {
      std::stack<std::vector<unsigned char>> stack;
      redefinitions_[name] = std::move(stack);
    }
    redefinitions_[name].push(std::move(data));
  }

  bool RetrieveRedefinition(std::string name, /*out*/std::vector<unsigned char>* data) {
    auto stack = redefinitions_.find(name);
    if (stack == redefinitions_.end() || stack->second.empty()) {
      return false;
    } else {
      *data = stack->second.top();
      return true;
    }
  }

  void PopRedefinition(std::string name) {
    if (should_pop_) {
      auto stack = redefinitions_.find(name);
      if (stack == redefinitions_.end() || stack->second.empty()) {
        return;
      } else {
        stack->second.pop();
      }
    }
  }

 private:
  std::unordered_map<std::string, std::stack<std::vector<unsigned char>>> redefinitions_;
  bool should_pop_;
};

static TransformationData data;

// The hook we are using.
void JNICALL CommonClassFileLoadHookRetransformable(jvmtiEnv* local_jvmti_env,
                                                    JNIEnv* jni_env ATTRIBUTE_UNUSED,
                                                    jclass class_being_redefined ATTRIBUTE_UNUSED,
                                                    jobject loader ATTRIBUTE_UNUSED,
                                                    const char* name,
                                                    jobject protection_domain ATTRIBUTE_UNUSED,
                                                    jint class_data_len ATTRIBUTE_UNUSED,
                                                    const unsigned char* class_dat ATTRIBUTE_UNUSED,
                                                    jint* new_class_data_len,
                                                    unsigned char** new_class_data) {
  std::string name_str(name);
  std::vector<unsigned char> dex_data;
  if (data.RetrieveRedefinition(name_str, &dex_data)) {
    unsigned char* jvmti_dex_data;
    if (JVMTI_ERROR_NONE == local_jvmti_env->Allocate(dex_data.size(), &jvmti_dex_data)) {
      memcpy(jvmti_dex_data, dex_data.data(), dex_data.size());
      *new_class_data_len = dex_data.size();
      *new_class_data = jvmti_dex_data;
      data.PopRedefinition(name);
    } else {
      LOG(FATAL) << "Unable to allocate output buffer for " << name;
    }
  }
}

extern "C"
JNIEXPORT void JNICALL Java_android_jvmti_cts_JvmtiRedefineClassesTest_setTransformationEvent(
    JNIEnv* env, jclass klass ATTRIBUTE_UNUSED, jboolean enable) {
  jvmtiEventCallbacks cb;
  memset(&cb, 0, sizeof(cb));
  cb.ClassFileLoadHook = CommonClassFileLoadHookRetransformable;
  if (JvmtiErrorToException(env, jvmti_env, jvmti_env->SetEventCallbacks(&cb, sizeof(cb)))) {
    return;
  }
  JvmtiErrorToException(env,
                        jvmti_env,
                        jvmti_env->SetEventNotificationMode(
                            enable == JNI_TRUE ? JVMTI_ENABLE : JVMTI_DISABLE,
                            JVMTI_EVENT_CLASS_FILE_LOAD_HOOK,
                            nullptr));
  return;
}

extern "C"
JNIEXPORT void JNICALL Java_android_jvmti_cts_JvmtiRedefineClassesTest_clearTransformations(
    JNIEnv* env ATTRIBUTE_UNUSED, jclass klass ATTRIBUTE_UNUSED) {
  data.ClearRedefinitions();
}

extern "C"
JNIEXPORT void JNICALL Java_android_jvmti_cts_JvmtiRedefineClassesTest_setPopTransformations(
    JNIEnv* env ATTRIBUTE_UNUSED, jclass klass ATTRIBUTE_UNUSED, jboolean enable) {
  data.SetPop(enable == JNI_TRUE ? true : false);
}

extern "C"
JNIEXPORT void JNICALL Java_android_jvmti_cts_JvmtiRedefineClassesTest_pushTransformationResult(
    JNIEnv* env, jclass klass ATTRIBUTE_UNUSED, jstring class_name, jbyteArray dex_bytes) {
  const char* name_chrs = env->GetStringUTFChars(class_name, nullptr);
  std::string name_str(name_chrs);
  env->ReleaseStringUTFChars(class_name, name_chrs);
  std::vector<unsigned char> dex_data;
  dex_data.resize(env->GetArrayLength(dex_bytes));
  signed char* redef_bytes = env->GetByteArrayElements(dex_bytes, nullptr);
  memcpy(dex_data.data(), redef_bytes, env->GetArrayLength(dex_bytes));
  data.PushRedefinition(std::move(name_str), std::move(dex_data));
  env->ReleaseByteArrayElements(dex_bytes, redef_bytes, 0);
}

static JNINativeMethod gMethods[] = {
  { "redefineClass", "(Ljava/lang/Class;[B)I",
          (void*)Java_android_jvmti_cts_JvmtiRedefineClassesTest_redefineClass },

  { "retransformClass", "(Ljava/lang/Class;)I",
          (void*)Java_android_jvmti_cts_JvmtiRedefineClassesTest_retransformClass },

  { "setTransformationEvent", "(Z)V",
          (void*)Java_android_jvmti_cts_JvmtiRedefineClassesTest_setTransformationEvent },

  { "clearTransformations", "()V",
          (void*)Java_android_jvmti_cts_JvmtiRedefineClassesTest_clearTransformations },

  { "setPopTransformations", "(Z)V",
          (void*)Java_android_jvmti_cts_JvmtiRedefineClassesTest_setPopTransformations },

  { "pushTransformationResult", "(Ljava/lang/String;[B)V",
          (void*)Java_android_jvmti_cts_JvmtiRedefineClassesTest_pushTransformationResult },
};

void register_android_jvmti_cts_JvmtiRedefineClassesTest(jvmtiEnv* jenv, JNIEnv* env) {
  ScopedLocalRef<jclass> klass(env, GetClass(jenv, env,
          "android/jvmti/cts/JvmtiRedefineClassesTest", nullptr));
  if (klass.get() == nullptr) {
    env->ExceptionClear();
    return;
  }

  env->RegisterNatives(klass.get(), gMethods, sizeof(gMethods) / sizeof(JNINativeMethod));
  if (env->ExceptionCheck()) {
    env->ExceptionClear();
    LOG(ERROR) << "Could not register natives for JvmtiRedefineClassesTest class";
  }
}

}  // namespace art