/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * 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.
 */


/** @file References.h
 *
 * Functionality similar to smart pointers, but for references into the VM. Four main reference
 * types are provided: local_ref, global_ref, weak_ref, and alias_ref. All are generic
 * templates that and refer to objects in the jobject hierarchy. The type of the referred objects
 * are specified using the template parameter. All reference types except alias_ref own their
 * underlying reference, just as a std smart pointer owns the underlying raw pointer. In the context
 * of std smart pointers, these references behave like unique_ptr, and have basically the same
 * interface. Thus, when the reference is destructed, the plain JNI reference, i.e. the underlying
 * JNI reference (like the parameters passed directly to JNI functions), is released. The alias
 * references provides no ownership and is a simple wrapper for plain JNI references.
 *
 * All but the weak references provides access to the underlying object using dereferencing, and a
 * get() method. It is also possible to convert these references to booleans to test for nullity.
 * To access the underlying object of a weak reference, the reference must either be released, or
 * the weak reference can be used to create a local or global reference.
 *
 * An owning reference is created either by moving the reference from an existing owned reference,
 * by copying an existing owned reference (which creates a new underlying reference), by using the
 * default constructor which initialize the reference to nullptr, or by using a helper function. The
 * helper function exist in two flavors: make_XXX or adopt_XXX.
 *
 * Adopting takes a plain JNI reference and wrap it in an owned reference. It takes ownership of the
 * plain JNI reference so be sure that no one else owns the reference when you adopt it, and make
 * sure that you know what kind of reference it is.
 *
 * New owned references can be created from existing plain JNI references, alias references, local
 * references, and global references (i.e. non-weak references) using the make_local, make_global,
 * and make_weak functions.
 *
 * Alias references can be implicitly initialized using global, local and plain JNI references using
 * the wrap_alias function. Here, we don't assume ownership of the passed-in reference, but rather
 * create a separate reference that we do own, leaving the passed-in reference to its fate.
 *
 * Similar rules apply for assignment. An owned reference can be copy or move assigned using a smart
 * reference of the same type. In the case of copy assignment a new reference is created. Alias
 * reference can also be assigned new values, but since they are simple wrappers of plain JNI
 * references there is no move semantics involved.
 *
 * Alias references are special in that they do not own the object and can therefore safely be
 * converted to and from its corresponding plain JNI reference. They are useful as parameters of
 * functions that do not affect the lifetime of a reference. Usage can be compared with using plain
 * JNI pointers as parameters where a function does not take ownership of the underlying object.
 *
 * The local, global, and alias references makes it possible to access methods in the underlying
 * objects. A core set of classes are implemented in CoreClasses.h, and user defined wrappers are
 * supported (see example below). The wrappers also supports inheritance so a wrapper can inherit
 * from another wrapper to gain access to its functionality. As an example the jstring wrapper
 * inherits from the jobject wrapper, so does the jclass wrapper. That means that you can for
 * example call the toString() method using the jclass wrapper, or any other class that inherits
 * from the jobject wrapper.
 *
 * Note that the wrappers are parameterized on the static type of your (jobject) pointer, thus if
 * you have a jobject that refers to a Java String you will need to cast it to jstring to get the
 * jstring wrapper. This also mean that if you make a down cast that is invalid there will be no one
 * stopping you and the wrappers currently does not detect this which can cause crashes. Thus, cast
 * wisely.
 *
 * @include WrapperSample.cpp
 */

#pragma once

#include <cassert>
#include <cstddef>
#include <type_traits>

#include <jni.h>

#include "ReferenceAllocators.h"
#include "TypeTraits.h"
#include "References-forward.h"

namespace facebook {
namespace jni {

/// Convenience function to wrap an existing local reference
template<typename T>
local_ref<T> adopt_local(T ref) noexcept;

/// Convenience function to wrap an existing global reference
template<typename T>
global_ref<T> adopt_global(T ref) noexcept;

/// Convenience function to wrap an existing weak reference
template<typename T>
weak_ref<T> adopt_weak_global(T ref) noexcept;


/// Swaps two owning references of the same type
template<typename T>
void swap(weak_ref<T>& a, weak_ref<T>& b) noexcept;

/// Swaps two owning references of the same type
template<typename T, typename Alloc>
void swap(basic_strong_ref<T, Alloc>& a, basic_strong_ref<T, Alloc>& b) noexcept;

/**
 * Retrieve the plain reference from a plain reference.
 */
template<typename T>
enable_if_t<IsPlainJniReference<T>(), T> getPlainJniReference(T ref);

/**
 * Retrieve the plain reference from an alias reference.
 */
template<typename T>
JniType<T> getPlainJniReference(alias_ref<T> ref);

/**
 * Retrieve the plain JNI reference from any reference owned reference.
 */
template<typename T, typename Alloc>
JniType<T> getPlainJniReference(const base_owned_ref<T, Alloc>& ref);

class JObject;
class JClass;

namespace detail {

template<typename T>
constexpr bool IsJavaClassType() {
  return std::is_base_of<JObject, T>::value;
}

template <typename T, typename Enable = void>
struct HasJniRefRepr : std::false_type {};

template <typename T>
struct HasJniRefRepr<T, typename std::enable_if<!std::is_same<typename T::JniRefRepr, void>::value, void>::type> : std::true_type {
  using type = typename T::JniRefRepr;
};

template <typename T>
struct RefReprType<T*> {
  static_assert(HasJniRefRepr<T>::value, "Repr type missing JniRefRepr.");
  using type = typename HasJniRefRepr<T>::type;
  static_assert(IsJavaClassType<type>(),
      "Repr type missing JObject base.");
  static_assert(std::is_same<type, typename RefReprType<type>::type>::value,
      "RefReprType<T> not idempotent");
};

template <typename T>
struct RefReprType<T, typename std::enable_if<IsJavaClassType<T>(), void>::type> {
  using type = T;
  static_assert(IsJavaClassType<type>(),
      "Repr type missing JObject base.");
  static_assert(std::is_same<type, typename RefReprType<type>::type>::value,
      "RefReprType<T> not idempotent");
};

template <typename T>
struct JavaObjectType {
  using type = typename RefReprType<T>::type::javaobject;
  static_assert(IsPlainJniReference<type>(),
      "JavaObjectType<T> not a plain jni reference");
  static_assert(std::is_same<type, typename JavaObjectType<type>::type>::value,
      "JavaObjectType<T> not idempotent");
};

template <typename T>
struct JavaObjectType<T*> {
  using type = T*;
  static_assert(IsPlainJniReference<type>(),
      "JavaObjectType<T> not a plain jni reference");
  static_assert(std::is_same<type, typename JavaObjectType<type>::type>::value,
      "JavaObjectType<T> not idempotent");
};

template <typename T>
struct PrimitiveOrJavaObjectType<T, enable_if_t<IsJniPrimitive<T>(), void>> {
  using type = T;
  static_assert(IsJniPrimitive<type>(),
      "PrimitiveOrJavaObjectType<T> not a jni primitive");
  static_assert(std::is_same<type, typename PrimitiveOrJavaObjectType<type>::type>::value,
      "PrimitiveOrJavaObjectType<T> not idempotent");
};

template <typename T>
struct PrimitiveOrJavaObjectType<T, enable_if_t<IsPlainJniReference<T>(), void>> {
  using type = T;
  static_assert(IsPlainJniReference<type>(),
      "PrimitiveOrJavaObjectType<T> not a plain jni reference");
  static_assert(std::is_same<type, typename PrimitiveOrJavaObjectType<type>::type>::value,
      "PrimitiveOrJavaObjectType<T> not idempotent");
};

template <typename T>
struct PrimitiveOrJavaObjectType<T, enable_if_t<IsJavaClassType<T>(), void>> {
  using type = JniType<T>;
  static_assert(IsPlainJniReference<type>(),
      "PrimitiveOrJavaObjectType<T> not a plain jni reference");
  static_assert(std::is_same<type, typename PrimitiveOrJavaObjectType<type>::type>::value,
      "PrimitiveOrJavaObjectType<T> not idempotent");
};

template <typename Repr>
struct ReprStorage {
  explicit ReprStorage(JniType<Repr> obj) noexcept;

  void set(JniType<Repr> obj) noexcept;

  Repr& get() noexcept;
  const Repr& get() const noexcept;
  JniType<Repr> jobj() const noexcept;

  void swap(ReprStorage& other) noexcept;

  ReprStorage() = delete;
  ReprStorage(const ReprStorage&) = delete;
  ReprStorage(ReprStorage&&) = delete;
  ReprStorage& operator=(const ReprStorage&) = delete;
  ReprStorage& operator=(ReprStorage&&) = delete;

 private:
  using Storage = typename std::aligned_storage<sizeof(JObjectBase), alignof(JObjectBase)>::type;
  Storage storage_;
};

} // namespace detail

/**
 * Create a new local reference from an existing reference
 *
 * @param ref a plain JNI, alias, or strong reference
 * @return an owned local reference (referring to null if the input does)
 * @throws std::bad_alloc if the JNI reference could not be created
 */
template<typename T>
enable_if_t<IsNonWeakReference<T>(), local_ref<plain_jni_reference_t<T>>>
make_local(const T& r);

/**
 * Create a new global reference from an existing reference
 *
 * @param ref a plain JNI, alias, or strong reference
 * @return an owned global reference (referring to null if the input does)
 * @throws std::bad_alloc if the JNI reference could not be created
 */
template<typename T>
enable_if_t<IsNonWeakReference<T>(), global_ref<plain_jni_reference_t<T>>>
make_global(const T& r);

/**
 * Create a new weak global reference from an existing reference
 *
 * @param ref a plain JNI, alias, or strong reference
 * @return an owned weak global reference (referring to null if the input does)
 * @throws std::bad_alloc if the returned reference is null
 */
template<typename T>
enable_if_t<IsNonWeakReference<T>(), weak_ref<plain_jni_reference_t<T>>>
make_weak(const T& r);

/**
 * Compare two references to see if they refer to the same object
 */
template<typename T1, typename T2>
enable_if_t<IsNonWeakReference<T1>() && IsNonWeakReference<T2>(), bool>
operator==(const T1& a, const T2& b);

/**
 * Compare two references to see if they don't refer to the same object
 */
template<typename T1, typename T2>
enable_if_t<IsNonWeakReference<T1>() && IsNonWeakReference<T2>(), bool>
operator!=(const T1& a, const T2& b);

/**
 * Compare references against nullptr
 */
template<typename T1>
enable_if_t<IsNonWeakReference<T1>(), bool>
operator==(const T1& a, std::nullptr_t);

template<typename T1>
enable_if_t<IsNonWeakReference<T1>(), bool>
operator==(std::nullptr_t, const T1& a);

template<typename T1>
enable_if_t<IsNonWeakReference<T1>(), bool>
operator!=(const T1& a, std::nullptr_t);

template<typename T1>
enable_if_t<IsNonWeakReference<T1>(), bool>
operator!=(std::nullptr_t, const T1& a);


template<typename T, typename Alloc>
class base_owned_ref {
 public:
  using javaobject = JniType<T>;

  /**
   * Release the ownership and set the reference to null. Thus no deleter is invoked.
   * @return Returns the reference
   */
  javaobject release() noexcept;

  /**
   * Reset the reference to refer to nullptr.
   */
  void reset() noexcept;

 protected:
  using Repr = ReprType<T>;
  detail::ReprStorage<Repr> storage_;

  javaobject get() const noexcept;
  void set(javaobject ref) noexcept;

  /*
   * Wrap an existing reference and transfers its ownership to the newly created unique reference.
   * NB! Does not create a new reference
   */
  explicit base_owned_ref(javaobject reference) noexcept;

  /// Create a null reference
  base_owned_ref() noexcept;

  /// Create a null reference
  explicit base_owned_ref(std::nullptr_t) noexcept;

  /// Copy constructor (note creates a new reference)
  base_owned_ref(const base_owned_ref& other);
  template<typename U>
  base_owned_ref(const base_owned_ref<U, Alloc>& other);

  /// Transfers ownership of an underlying reference from one unique reference to another
  base_owned_ref(base_owned_ref&& other) noexcept;
  template<typename U>
  base_owned_ref(base_owned_ref<U, Alloc>&& other) noexcept;

  /// The delete the underlying reference if applicable
  ~base_owned_ref() noexcept;


  /// Assignment operator (note creates a new reference)
  base_owned_ref& operator=(const base_owned_ref& other);

  /// Assignment by moving a reference thus not creating a new reference
  base_owned_ref& operator=(base_owned_ref&& rhs) noexcept;

  void reset(javaobject reference) noexcept;

  friend javaobject jni::getPlainJniReference<>(const base_owned_ref<T, Alloc>& ref);

  template<typename U, typename UAlloc>
  friend class base_owned_ref;
};


/**
 * A smart reference that owns its underlying JNI reference. The class provides basic
 * functionality to handle a reference but gives no access to it unless the reference is
 * released, thus no longer owned. The API is stolen with pride from unique_ptr and the
 * semantics should be basically the same. This class should not be used directly, instead use
 * @ref weak_ref
 */
template<typename T>
class weak_ref : public base_owned_ref<T, WeakGlobalReferenceAllocator> {
 public:
  using javaobject = JniType<T>;

  using Allocator = WeakGlobalReferenceAllocator;

  // This inherits non-default, non-copy, non-move ctors.
  using base_owned_ref<T, Allocator>::base_owned_ref;

  /// Create a null reference
  weak_ref() noexcept
    : base_owned_ref<T, Allocator>{} {}

  /// Create a null reference
  /* implicit */ weak_ref(std::nullptr_t) noexcept
    : base_owned_ref<T, Allocator>{nullptr} {}

  /// Copy constructor (note creates a new reference)
  weak_ref(const weak_ref& other)
    : base_owned_ref<T, Allocator>{other} {}

  // This needs to be explicit to change its visibility.
  template<typename U>
  weak_ref(const weak_ref<U>& other)
    : base_owned_ref<T, Allocator>{other} {}

  /// Transfers ownership of an underlying reference from one unique reference to another
  weak_ref(weak_ref&& other) noexcept
    : base_owned_ref<T, Allocator>{std::move(other)} {}

  // Move from ref to compatible type.
  template<typename U>
  weak_ref(weak_ref<U>&& other)
      : base_owned_ref<T, Allocator>{std::move(other)} {}

  /// Assignment operator (note creates a new reference)
  weak_ref& operator=(const weak_ref& other);

  /// Assignment by moving a reference thus not creating a new reference
  weak_ref& operator=(weak_ref&& rhs) noexcept;

  // Creates an owned local reference to the referred object or to null if the object is reclaimed
  local_ref<T> lockLocal() const;

  // Creates an owned global reference to the referred object or to null if the object is reclaimed
  global_ref<T> lockGlobal() const;

 private:
  // get/release/reset on weak_ref are not exposed to users.
  using base_owned_ref<T, Allocator>::get;
  using base_owned_ref<T, Allocator>::release;
  using base_owned_ref<T, Allocator>::reset;
  /*
   * Wrap an existing reference and transfers its ownership to the newly created unique reference.
   * NB! Does not create a new reference
   */
  explicit weak_ref(javaobject reference) noexcept
    : base_owned_ref<T, Allocator>{reference} {}

  template<typename T2> friend class weak_ref;
  friend weak_ref<javaobject> adopt_weak_global<javaobject>(javaobject ref) noexcept;
  friend void swap<T>(weak_ref& a, weak_ref& b) noexcept;
};


/**
 * A class representing owned strong references to Java objects. This class
 * should not be used directly, instead use @ref local_ref, or @ref global_ref.
 */
template<typename T, typename Alloc>
class basic_strong_ref : public base_owned_ref<T, Alloc> {
  using typename base_owned_ref<T, Alloc>::Repr;
 public:
  using javaobject = JniType<T>;

  using Allocator = Alloc;

  // This inherits non-default, non-copy, non-move ctors.
  using base_owned_ref<T, Alloc>::base_owned_ref;
  using base_owned_ref<T, Alloc>::release;
  using base_owned_ref<T, Alloc>::reset;

  /// Create a null reference
  basic_strong_ref() noexcept
    : base_owned_ref<T, Alloc>{} {}

  /// Create a null reference
  /* implicit */ basic_strong_ref(std::nullptr_t) noexcept
    : base_owned_ref<T, Alloc>{nullptr} {}

  /// Copy constructor (note creates a new reference)
  basic_strong_ref(const basic_strong_ref& other)
    : base_owned_ref<T, Alloc>{other} {}

  // This needs to be explicit to change its visibility.
  template<typename U>
  basic_strong_ref(const basic_strong_ref<U, Alloc>& other)
    : base_owned_ref<T, Alloc>{other} {}

  // Move from ref to compatible type.
  template<typename U>
  basic_strong_ref(basic_strong_ref<U, Alloc>&& other)
    : base_owned_ref<T, Alloc>{std::move(other)} {}

  /// Transfers ownership of an underlying reference from one unique reference to another
  basic_strong_ref(basic_strong_ref&& other) noexcept
    : base_owned_ref<T, Alloc>{std::move(other)} {}

  /// Assignment operator (note creates a new reference)
  basic_strong_ref& operator=(const basic_strong_ref& other);

  /// Assignment by moving a reference thus not creating a new reference
  basic_strong_ref& operator=(basic_strong_ref&& rhs) noexcept;

  /// Get the plain JNI reference
  using base_owned_ref<T, Allocator>::get;

  /// Release the ownership of the reference and return the wrapped reference in an alias
  alias_ref<T> releaseAlias() noexcept;

  /// Checks if the reference points to a non-null object
  explicit operator bool() const noexcept;

  /// Access the functionality provided by the object wrappers
  Repr* operator->() noexcept;

  /// Access the functionality provided by the object wrappers
  const Repr* operator->() const noexcept;

  /// Provide a reference to the underlying wrapper (be sure that it is non-null before invoking)
  Repr& operator*() noexcept;

  /// Provide a const reference to the underlying wrapper (be sure that it is non-null
  /// before invoking)
  const Repr& operator*() const noexcept;

 private:

  using base_owned_ref<T, Alloc>::storage_;

  /*
   * Wrap an existing reference and transfers its ownership to the newly created unique reference.
   * NB! Does not create a new reference
   */
  explicit basic_strong_ref(javaobject reference) noexcept
    : base_owned_ref<T, Alloc>{reference} {}


  friend local_ref<T> adopt_local<T>(T ref) noexcept;
  friend global_ref<T> adopt_global<T>(T ref) noexcept;
  friend void swap<T, Alloc>(basic_strong_ref& a, basic_strong_ref& b) noexcept;
};


template<typename T>
enable_if_t<IsPlainJniReference<T>(), alias_ref<T>> wrap_alias(T ref) noexcept;

/// Swaps to alias reference of the same type
template<typename T>
void swap(alias_ref<T>& a, alias_ref<T>& b) noexcept;

/**
 * A non-owning variant of the smart references (a dumb
 * reference). Use this representation when you don't want to claim
 * ownership of the underlying reference (compare to using raw
 * pointers instead of smart pointers.)
 */
template<typename T>
class alias_ref {
  using Repr = ReprType<T>;

 public:
  using javaobject = JniType<T>;

  /// Create a null reference
  alias_ref() noexcept;

  /// Create a null reference
  /* implicit */ alias_ref(std::nullptr_t) noexcept;

  /// Copy constructor
  alias_ref(const alias_ref& other) noexcept;

  /// Wrap an existing plain JNI reference
  /* implicit */ alias_ref(javaobject ref) noexcept;

  /// Wrap an existing smart reference of any type convertible to T
  template<
    typename TOther,
    typename = enable_if_t<
      IsConvertible<JniType<TOther>, javaobject>(), T>
    >
  alias_ref(alias_ref<TOther> other) noexcept;

  /// Wrap an existing alias reference of a type convertible to T
  template<
    typename TOther,
    typename AOther,
    typename = enable_if_t<
      IsConvertible<JniType<TOther>, javaobject>(), T>
    >
  alias_ref(const basic_strong_ref<TOther, AOther>& other) noexcept;

  /// Assignment operator
  alias_ref& operator=(alias_ref other) noexcept;

  /// Checks if the reference points to a non-null object
  explicit operator bool() const noexcept;

  /// Converts back to a plain JNI reference
  javaobject get() const noexcept;

  /// Access the functionality provided by the object wrappers
  Repr* operator->() noexcept;

  /// Access the functionality provided by the object wrappers
  const Repr* operator->() const noexcept;

  /// Provide a guaranteed non-null reference (be sure that it is non-null before invoking)
  Repr& operator*() noexcept;

  /// Provide a guaranteed non-null reference (be sure that it is non-null before invoking)
  const Repr& operator*() const noexcept;

 private:
  void set(javaobject ref) noexcept;

  detail::ReprStorage<Repr> storage_;

  friend void swap<T>(alias_ref& a, alias_ref& b) noexcept;
};


/**
 * RAII object to create a local JNI frame, using PushLocalFrame/PopLocalFrame.
 *
 * This is useful when you have a call which is initiated from C++-land, and therefore
 * doesn't automatically get a local JNI frame managed for you by the JNI framework.
 */
class JniLocalScope {
public:
  JniLocalScope(JNIEnv* p_env, jint capacity);
  ~JniLocalScope();

private:
  JNIEnv* env_;
  bool hasFrame_;
};

template<typename T, typename U>
enable_if_t<IsPlainJniReference<JniType<T>>(), local_ref<T>>
static_ref_cast(const local_ref<U>& ref) noexcept;

template<typename T, typename U>
enable_if_t<IsPlainJniReference<JniType<T>>(), global_ref<T>>
static_ref_cast(const global_ref<U>& ref) noexcept;

template<typename T, typename U>
enable_if_t<IsPlainJniReference<JniType<T>>(), alias_ref<T>>
static_ref_cast(const alias_ref<U>& ref) noexcept;

template<typename T, typename RefType>
auto dynamic_ref_cast(const RefType& ref) ->
enable_if_t<IsPlainJniReference<JniType<T>>(), decltype(static_ref_cast<T>(ref))> ;

}}

#include "References-inl.h"
