/*
 * Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "jvmti.h"
#include "jni_tools.hpp"
#include "agent_common.hpp"
#include "JVMTITools.hpp"

extern "C" {


#define PASSED 0
#define STATUS_FAILED 2

static jvmtiEnv *jvmti = nullptr;
static jint result = PASSED;
static jboolean printdump = JNI_FALSE;
static jvmtiCapabilities jvmti_caps;
static jint dummy_user_data = 0;
static jboolean user_data_error_flag = JNI_FALSE;

#define HEAP_ROOT_REF_KIND_BASE 100
#define MISSED_REF_KIND_BASE 300

typedef enum {
  rthread,
  rclass,
  rother,
  rmark
} refKind;

struct _refLink;

typedef struct _myTag {
  refKind kind;
  const struct _myTag* class_tag;
  jlong size;
  jlong sequence;
  jboolean visited;
  const char* name;
  struct _refLink *ref;
} MyTag;

typedef struct _refLink {
  MyTag* tag;
  int reference_kind;
  struct _refLink *next;
} refLink;

static MyTag *fakeRoot = nullptr;
static MyTag *missed = nullptr;

static void breakpoint() {
  printf("Continuing from BREAKPOINT\n");
}

static MyTag *newTag(refKind kind,
                     const MyTag* class_tag,
                     jlong size,
                     const char* name) {
  static jlong seq_num = 0;
  MyTag* new_tag = nullptr;

  new_tag = (MyTag*) malloc(sizeof(MyTag));
  if (nullptr == new_tag) {
    printf("Error (newTag malloc): failed\n");
    result = STATUS_FAILED;
  }
  new_tag->kind = kind;
  new_tag->class_tag = class_tag;
  new_tag->size = size;
  new_tag->sequence = ++seq_num;
  new_tag->visited = JNI_FALSE;
  new_tag->name = name;
  new_tag->ref = nullptr;
  return new_tag;
}

static void setTag(JNIEnv *env,
                   jobject obj,
                   refKind kind,
                   const char* name) {
  MyTag *new_tag = nullptr;
  MyTag *class_tag = nullptr;
  jvmtiError err;
  jlong size = 0;
  jclass obj_class = nullptr;
  jlong haba = 0;

  err = jvmti->GetObjectSize(obj, &size);
  if (err != JVMTI_ERROR_NONE) {
    printf("Error (ObjectSize): %s (%d)\n", TranslateError(err), err);
    result = STATUS_FAILED;
  }

  obj_class = env->GetObjectClass(obj);

  err = jvmti->GetTag(obj_class, &haba);
  class_tag = (MyTag*)(intptr_t)haba;
  if (err != JVMTI_ERROR_NONE) {
    printf("Error (GetTag): %s (%d)\n", TranslateError(err), err);
    result = STATUS_FAILED;
  }
  if (class_tag != nullptr && class_tag->kind != rclass) {
    printf("Error class tag which is not a class\n");
    result = STATUS_FAILED;
  }

  new_tag = newTag(kind, class_tag, size, name);

  err = jvmti->SetTag(obj, (intptr_t)new_tag);
  if (err != JVMTI_ERROR_NONE) {
    printf("Error (SetTag): %s (%d)\n", TranslateError(err), err);
    result = STATUS_FAILED;
  }
}

static void addRef(MyTag *from, int reference_kind, MyTag *to) {
  refLink *new_ref;

  new_ref = (refLink*) malloc(sizeof(refLink));
  if (nullptr == new_ref) {
    printf("Error (addRef malloc): failed\n");
    result = STATUS_FAILED;
  }
  new_ref->tag = to;
  new_ref->reference_kind = reference_kind;
  new_ref->next = from->ref;
  from->ref = new_ref;
}

static const char* reference_label(refLink *link) {
  int reference_kind = link->reference_kind;
  const char *name = "<font color=\"red\">**unknown**</font>";
  switch (reference_kind) {
    case JVMTI_REFERENCE_CLASS:
      name = "<font color=\"black\">class</font>";
      break;
    case JVMTI_REFERENCE_FIELD:
      name = "<font color=\"black\">field</font>";
      break;
    case JVMTI_REFERENCE_ARRAY_ELEMENT:
      name = "<font color=\"green\">array_element</font>";
      break;
    case JVMTI_REFERENCE_CLASS_LOADER:
      name = "<font color=\"purple\">class_loader</font>";
      break;
    case JVMTI_REFERENCE_SIGNERS:
      name = "<font color=\"purple\">signers</font>";
      break;
    case JVMTI_REFERENCE_PROTECTION_DOMAIN:
      name = "<font color=\"purple\">protection_domain</font>";
      break;
    case JVMTI_REFERENCE_INTERFACE:
      name = "<font color=\"purple\">interface</font>";
      break;
    case JVMTI_REFERENCE_STATIC_FIELD:
      name = "<font color=\"black\">static_field</font>";
      break;
    case HEAP_ROOT_REF_KIND_BASE+JVMTI_HEAP_ROOT_JNI_GLOBAL:
      name = "<font color=\"orange\">root::jni_global</font>";
      break;
    case HEAP_ROOT_REF_KIND_BASE+JVMTI_HEAP_ROOT_SYSTEM_CLASS:
      name = "<font color=\"orange\">root::system_class</font>";
      break;
    case HEAP_ROOT_REF_KIND_BASE+JVMTI_HEAP_ROOT_MONITOR:
      name = "<font color=\"orange\">root::monitor</font>";
      break;
    case HEAP_ROOT_REF_KIND_BASE+JVMTI_HEAP_ROOT_STACK_LOCAL:
      name = "<font color=\"orange\">root::local_var</font>";
      break;
    case HEAP_ROOT_REF_KIND_BASE+JVMTI_HEAP_ROOT_JNI_LOCAL:
      name = "<font color=\"orange\">root::jni_local</font>";
      break;
    case HEAP_ROOT_REF_KIND_BASE+JVMTI_HEAP_ROOT_THREAD:
      name = "<font color=\"orange\">root::thread</font>";
      break;
    case HEAP_ROOT_REF_KIND_BASE+JVMTI_HEAP_ROOT_OTHER:
      name = "<font color=\"orange\">root::other</font>";
      break;
    default:
      printf("Error: Unexpected reference kind %d\n", reference_kind);
      result = STATUS_FAILED;
      break;
  }
  return name;
}

static void walk(MyTag* tag, jint depth, const char* ref_label) {
  static const char* const spaces = ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ";
  static const int len = 86;
  const char *indent = spaces + (len - 2 * depth);

  const MyTag* const ctag = tag->class_tag;
  const char* const cname = ctag != nullptr ? ctag->name : "";

  printf("%s", indent);

  if (tag->visited) {
    printf("<a href=\"#%" LL "d\">", tag->sequence);
  } else {
    printf("<a name=\"%" LL "d\">", tag->sequence);
  }
  if (tag->name) {
    printf("<b>%s(%s)</b>", cname, tag->name);
  } else {
    printf("%s(%" LL "d)", cname, tag->sequence);
  }
  printf("</a> -- ");
  printf("%s\n", ref_label);
  if (!tag->visited) {
    refLink *ref;
    tag->visited = JNI_TRUE;
    for (ref = tag->ref; ref; ref = ref->next) {
      walk(ref->tag, depth + 1, reference_label(ref));
    }
  }
}

#ifdef STATIC_BUILD
JNIEXPORT jint JNICALL Agent_OnLoad_refignore(JavaVM *jvm, char *options, void *reserved) {
  return Agent_Initialize(jvm, options, reserved);
}
JNIEXPORT jint JNICALL Agent_OnAttach_refignore(JavaVM *jvm, char *options, void *reserved) {
  return Agent_Initialize(jvm, options, reserved);
}
JNIEXPORT jint JNI_OnLoad_refignore(JavaVM *jvm, char *options, void *reserved) {
  return JNI_VERSION_1_8;
}
#endif

jint Agent_Initialize(JavaVM *jvm, char *options, void *reserved) {
  jint res;
  jvmtiError err;

  if (options != nullptr && strcmp(options, "printdump") == 0) {
    printdump = JNI_TRUE;
  }

  res = jvm->GetEnv((void **) &jvmti, JVMTI_VERSION_1_1);
  if (res != JNI_OK || jvmti == nullptr) {
    printf("Wrong result of a valid call to GetEnv!\n");
    return JNI_ERR;
  }

  memset((void*)&jvmti_caps, 0, sizeof(jvmtiCapabilities));
  jvmti_caps.can_tag_objects = 1;
  err = jvmti->AddCapabilities(&jvmti_caps);
  if (err != JVMTI_ERROR_NONE) {
    printf("Error (AddCapabilities): %s (%d)\n", TranslateError(err), err);
    return JNI_ERR;
  }

  return JNI_OK;
}

jvmtiIterationControl JNICALL
heapMarkCallback(jlong class_tag, jlong size, jlong* tag_ptr, void* user_data) {
  const MyTag* const tag = newTag(rmark, (const MyTag*)(intptr_t)class_tag, size, nullptr);
  *tag_ptr = (intptr_t)tag;

  if (user_data != &dummy_user_data && user_data_error_flag == JNI_FALSE) {
    user_data_error_flag = JNI_TRUE;
    printf("Error (heapMarkCallback): unexpected value of user_data\n");
    result = STATUS_FAILED;
  }
  return JVMTI_ITERATION_CONTINUE;
}

jvmtiIterationControl JNICALL
heapRootCallback(jvmtiHeapRootKind root_kind,
                 jlong class_tag, jlong size,
                 jlong* tag_ptr, void* user_data) {
  refKind kind = rother;

  if (0 == *tag_ptr) {
    /* new tag */
    MyTag* tag = newTag(kind, (MyTag*)(intptr_t)class_tag, size, nullptr);
    addRef(fakeRoot, HEAP_ROOT_REF_KIND_BASE+root_kind, tag);
    *tag_ptr = (intptr_t)tag;
  } else {
    /* existing tag */
    addRef(fakeRoot, HEAP_ROOT_REF_KIND_BASE+root_kind, (MyTag*)(intptr_t)*tag_ptr);
  }

  if (user_data != &dummy_user_data && user_data_error_flag == JNI_FALSE) {
    user_data_error_flag = JNI_TRUE;
    printf("Error (heapRootCallback): unexpected value of user_data\n");
    result = STATUS_FAILED;
  }
  return JVMTI_ITERATION_CONTINUE;
}

jvmtiIterationControl JNICALL
stackReferenceCallback(jvmtiHeapRootKind root_kind,
                       jlong class_tag, jlong size,
                       jlong* tag_ptr, jlong thread_tag,
                       jint depth, jmethodID method,
                       jint slot, void* user_data) {
  refKind kind = rother;

  if (0 == *tag_ptr) {
    /* new tag */
    MyTag* tag = newTag(kind, (MyTag*)(intptr_t)class_tag, size, nullptr);
    addRef(fakeRoot, HEAP_ROOT_REF_KIND_BASE+root_kind, tag);
    *tag_ptr = (intptr_t)tag;
  } else {
    /* existing tag */
    addRef(fakeRoot, HEAP_ROOT_REF_KIND_BASE+root_kind, (MyTag*)(intptr_t)*tag_ptr);
  }
  if (user_data != &dummy_user_data && user_data_error_flag == JNI_FALSE) {
    user_data_error_flag = JNI_TRUE;
    printf("Error (stackReferenceCallback): unexpected value of user_data\n");
    result = STATUS_FAILED;
  }
  return JVMTI_ITERATION_CONTINUE;
}

jvmtiIterationControl JNICALL
objectReferenceCallback(jvmtiObjectReferenceKind reference_kind,
                        jlong class_tag, jlong size,
                        jlong* tag_ptr, jlong referrer_tag,
                        jint referrer_index, void* user_data) {
  refKind kind = rother;
  MyTag* referrer = nullptr;

  if (0 == referrer_tag) {
    referrer = missed;
  } else {
    referrer = (MyTag *)(intptr_t)referrer_tag;
  }

  if (0 == *tag_ptr) {
    /* new tag */
    MyTag* tag = newTag(kind, (MyTag*)(intptr_t)class_tag, size, nullptr);
    addRef(referrer, reference_kind, tag);
    *tag_ptr = (intptr_t) tag;
  } else {
    /* existing tag */
    MyTag* tag = (MyTag*)(intptr_t)*tag_ptr;
    addRef(referrer, reference_kind, tag);
  }
  if (user_data != &dummy_user_data && user_data_error_flag == JNI_FALSE) {
    user_data_error_flag = JNI_TRUE;
    printf("Error (objectReferenceCallback): unexpected value of user_data\n");
    result = STATUS_FAILED;
  }
  return JVMTI_ITERATION_IGNORE;
}

JNIEXPORT jint JNICALL
Java_nsk_jvmti_unit_refignore_check(JNIEnv *env, jclass cls) {
  jvmtiError err;
  jclass *classes;
  jint classCount = 0;
  jthread *threads;
  jint threadCount = 0;
  jint i;

  if (jvmti == nullptr) {
    printf("JVMTI client was not properly loaded!\n");
    return STATUS_FAILED;
  }

  fakeRoot = newTag(rother, nullptr, 0, "FAKE_ROOT");
  missed = newTag(rother, nullptr, 0, "MISSED");

  if (env->PushLocalFrame(500) != 0) {
    printf("Error (PushLocalFrame): failed\n");
    result = STATUS_FAILED;
  }

  err = jvmti->GetLoadedClasses(&classCount, &classes);
  if (err != JVMTI_ERROR_NONE) {
    printf("Error (GetLoadedClasses): %s (%d)\n", TranslateError(err), err);
    result = STATUS_FAILED;
  }

  for (i = 0; i < classCount; ++i) {
    char *classSig;
    jclass k = classes[i];
    err = jvmti->GetClassSignature(k, &classSig, nullptr);
    if (err != JVMTI_ERROR_NONE) {
      printf("Error (getClassSignature): %s (%d)\n", TranslateError(err), err);
      result = STATUS_FAILED;
    } else {
      char* slash = strrchr(classSig, '/');
      const size_t len = strlen(classSig);
      if (classSig[len-1] == ';') {
        classSig[len-1] = 0;
      }
      if (*classSig == 'L' && slash != nullptr) {
        classSig = slash + 1;
      }
      setTag(env, k, rclass, (const char*)classSig);
    }
  }

  err = jvmti->GetAllThreads(&threadCount, &threads);
  if (err != JVMTI_ERROR_NONE) {
    printf("Error (GetAllThreads): %s (%d)\n", TranslateError(err), err);
    result = STATUS_FAILED;
  }

  for (i = 0; i < threadCount; ++i) {
    jvmtiThreadInfo info;
    jthread t = threads[i];
    err = jvmti->GetThreadInfo(t, &info);
    if (err != JVMTI_ERROR_NONE) {
      printf("Error (GetThreadInfo): %s (%d)\n", TranslateError(err), err);
      result = STATUS_FAILED;
    } else {
      setTag(env, t, rthread, (const char*)info.name);
    }
  }

  env->PopLocalFrame(nullptr);

  user_data_error_flag = JNI_FALSE;
  err = jvmti->IterateOverHeap(
                                  JVMTI_HEAP_OBJECT_UNTAGGED,
                                  heapMarkCallback,
                                  &dummy_user_data);
  if (err != JVMTI_ERROR_NONE) {
    printf("Error (IterateOverHeap): %s (%d)\n", TranslateError(err), err);
    result = STATUS_FAILED;
  }

  user_data_error_flag = JNI_FALSE;
  err = jvmti->IterateOverReachableObjects(
                                              heapRootCallback,
                                              stackReferenceCallback,
                                              objectReferenceCallback,
                                              &dummy_user_data);
  if (err != JVMTI_ERROR_NONE) {
    printf("Error (IterateOverReachableObjects): %s (%d)\n", TranslateError(err), err);
    result = STATUS_FAILED;
  }

  if (printdump == JNI_TRUE) {
    printf("<html><head><title>Heap Dump</title></head><body><pre>\n");
    walk(fakeRoot, 0, "roots");
    printf("\n------------------- MISSED ------------------\n\n");
    walk(missed, 0, "missed");
    printf("</pre></body></html>\n");
  }

  return result;
}

}
