/*
 *  Copyright (c) 2018-present, Facebook, Inc.
 *
 *  This source code is licensed under the MIT license found in the LICENSE
 *  file in the root directory of this source tree.
 *
 */
#pragma once
#include <functional>
#include <string>
#include <jni.h>


namespace facebook {
namespace jni {

// Keeps a thread-local reference to the current thread's JNIEnv.
struct Environment {
  // Throws a std::runtime_error if this thread isn't attached to the JVM
  // TODO(T6594868) Benchmark against raw JNI access
  static JNIEnv* current();
  static void initialize(JavaVM* vm);

  // There are subtle issues with calling the next functions directly. It is
  // much better to always use a ThreadScope to manage attaching/detaching for
  // you.
  static JNIEnv* ensureCurrentThreadIsAttached();
};

namespace detail {

// This will return null the thread isn't attached to the VM, or if
// fbjni has never been initialized with a VM at all.  You probably
// shouldn't be using this.
JNIEnv* currentOrNull();

/**
 * If there's thread-local data, it's a pointer to one of these.  The
 * instance is a member of JniEnvCacher or ThreadScope, and lives on
 * the stack.
 */
struct TLData {
  // This is modified only by JniEnvCacher, and is guaranteed to be
  // valid if set, and refer to an env which originated from a JNI
  // call into C++.
  JNIEnv* env;
  // This is modified only by ThreadScope, and is set only if an
  // instance of ThreadScope which attached is on the stack.
  bool attached;
};

/**
 * RAII object which manages a cached JNIEnv* value.  A Value is only
 * cached if it is guaranteed safe, which means when C++ is called
 * from a registered fbjni function.
 */
class JniEnvCacher {
public:
  JniEnvCacher(JNIEnv* env);
  JniEnvCacher(JniEnvCacher&) = delete;
  JniEnvCacher(JniEnvCacher&&) = default;
  JniEnvCacher& operator=(JniEnvCacher&) = delete;
  JniEnvCacher& operator=(JniEnvCacher&&) = delete;
  ~JniEnvCacher();

private:
  // If this flag is set, then, this object needs to clear the cache.
  bool thisCached_;

  // The thread local pointer may point here.
  detail::TLData data_;
};

}

/**
 * RAII Object that attaches a thread to the JVM. Failing to detach from a thread before it
 * exits will cause a crash, as will calling Detach an extra time, and this guard class helps
 * keep that straight. In addition, it remembers whether it performed the attach or not, so it
 * is safe to nest it with itself or with non-fbjni code that manages the attachment correctly.
 *
 * Potential concerns:
 *  - Attaching to the JVM is fast (~100us on MotoG), but ideally you would attach while the
 *    app is not busy.
 *  - Having a thread detach at arbitrary points is not safe in Dalvik; you need to be sure that
 *    there is no Java code on the current stack or you run the risk of a crash like:
 *      ERROR: detaching thread with interp frames (count=18)
 *    (More detail at https://groups.google.com/forum/#!topic/android-ndk/2H8z5grNqjo)
 *    ThreadScope won't do a detach if the thread was already attached before the guard is
 *    instantiated, but there's probably some usage that could trip this up.
 *  - Newly attached C++ threads only get the bootstrap class loader -- i.e. java language
 *    classes, not any of our application's classes. This will be different behavior than threads
 *    that were initiated on the Java side. A workaround is to pass a global reference for a
 *    class or instance to the new thread; this bypasses the need for the class loader.
 *    (See http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/invocation.html#attach_current_thread)
 *    If you need access to the application's classes, you can use ThreadScope::WithClassLoader.
 *  - If fbjni has never been initialized, there will be no JavaVM object to attach with.
 *    In that case, a std::runtime_error will be thrown.  This is only likely to happen in a
 *    standalone C++ application, or if Environment::initialize is not used.
 */
class ThreadScope {
 public:
  ThreadScope();
  ThreadScope(ThreadScope&) = delete;
  ThreadScope(ThreadScope&&) = default;
  ThreadScope& operator=(ThreadScope&) = delete;
  ThreadScope& operator=(ThreadScope&&) = delete;
  ~ThreadScope();

  /**
   * This runs the closure in a scope with fbjni's classloader. This should be
   * the same classloader as the rest of the application and thus anything
   * running in the closure will have access to the same classes as in a normal
   * java-create thread.
   */
  static void WithClassLoader(std::function<void()>&& runnable);

  static void OnLoad();

 private:
  // If this flag is set, then this object needs to detach.
  bool thisAttached_;

  // The thread local pointer may point here.
  detail::TLData data_;
};

}
}
