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

#pragma once

#include <jni.h>

#include <fbjni/detail/SimpleFixedString.h>

#include "Common.h"
#include "Exceptions.h"
#include "MetaConvert.h"
#include "References.h"
#include "Boxed.h"

namespace facebook {
namespace jni {

// JMethod /////////////////////////////////////////////////////////////////////////////////////////

inline JMethodBase::JMethodBase(jmethodID method_id) noexcept
  : method_id_{method_id}
{}

inline JMethodBase::operator bool() const noexcept {
  return method_id_ != nullptr;
}

inline jmethodID JMethodBase::getId() const noexcept {
  return method_id_;
}

namespace {

template <int idx, typename... Args>
struct ArgsArraySetter;

template <int idx, typename Arg, typename... Args>
struct ArgsArraySetter<idx, Arg, Args...> {
  static void set(alias_ref<JArrayClass<jobject>> array, Arg arg0, Args... args) {
    // TODO(xxxxxxxx): Use Convert<Args>... to do conversions like the fast path.
    (*array)[idx] = autobox(arg0);
    ArgsArraySetter<idx + 1, Args...>::set(array, args...);
  }
};

template <int idx>
struct ArgsArraySetter<idx> {
  static void set(alias_ref<JArrayClass<jobject>> array) {
    (void)array;
  }
};

template <typename... Args>
local_ref<JArrayClass<jobject>> makeArgsArray(Args... args) {
  auto arr = JArrayClass<jobject>::newArray(sizeof...(args));
  ArgsArraySetter<0, Args...>::set(arr, args...);
  return arr;
}

}

template<typename... Args>
inline void JMethod<void(Args...)>::operator()(alias_ref<jobject> self, Args... args) const {
  const auto env = Environment::current();
  env->CallVoidMethod(
        self.get(),
        getId(),
        detail::callToJni(detail::Convert<typename std::decay<Args>::type>::toCall(args))...);
  FACEBOOK_JNI_THROW_PENDING_EXCEPTION();
}

#pragma push_macro("DEFINE_PRIMITIVE_CALL")
#undef DEFINE_PRIMITIVE_CALL
#define DEFINE_PRIMITIVE_CALL(TYPE, METHOD)                                                    \
template<typename... Args>                                                                     \
inline TYPE JMethod<TYPE(Args...)>::operator()(alias_ref<jobject> self, Args... args) const {  \
  const auto env = Environment::current();                                                     \
  auto result = env->Call ## METHOD ## Method(                                                 \
        self.get(),                                                                            \
        getId(),                                                                               \
        detail::callToJni(detail::Convert<typename std::decay<Args>::type>::toCall(args))...); \
  FACEBOOK_JNI_THROW_PENDING_EXCEPTION();                                                      \
  return result;                                                                               \
}

DEFINE_PRIMITIVE_CALL(jboolean, Boolean)
DEFINE_PRIMITIVE_CALL(jbyte, Byte)
DEFINE_PRIMITIVE_CALL(jchar, Char)
DEFINE_PRIMITIVE_CALL(jshort, Short)
DEFINE_PRIMITIVE_CALL(jint, Int)
DEFINE_PRIMITIVE_CALL(jlong, Long)
DEFINE_PRIMITIVE_CALL(jfloat, Float)
DEFINE_PRIMITIVE_CALL(jdouble, Double)
#pragma pop_macro("DEFINE_PRIMITIVE_CALL")

/// JMethod specialization for references that wraps the return value in a @ref local_ref
template<typename R, typename... Args>
class JMethod<R(Args...)> : public JMethodBase {
 public:
   // TODO: static_assert is jobject-derived or local_ref jobject
  using JniRet = typename detail::Convert<typename std::decay<R>::type>::jniType;
  static_assert(
      IsPlainJniReference<JniRet>() || detail::IsJavaClassType<JniRet>(),
      "Return type must be a JNI reference or JavaClass type.");
  using JMethodBase::JMethodBase;
  JMethod() noexcept {};
  JMethod(const JMethod& other) noexcept = default;

  /// Invoke a method and return a local reference wrapping the result
  local_ref<JniRet> operator()(alias_ref<jobject> self, Args... args) const;

  friend class JClass;
};

template<typename R, typename... Args>
inline auto JMethod<R(Args...)>::operator()(alias_ref<jobject> self, Args... args) const -> local_ref<JniRet> {
  const auto env = Environment::current();
  auto result = env->CallObjectMethod(
      self.get(),
      getId(),
      detail::callToJni(detail::Convert<typename std::decay<Args>::type>::toCall(args))...);
  FACEBOOK_JNI_THROW_PENDING_EXCEPTION();
  return adopt_local(static_cast<JniType<JniRet>>(result));
}

template<typename... Args>
inline void JStaticMethod<void(Args...)>::operator()(alias_ref<jclass> cls, Args... args) const {
  const auto env = Environment::current();
  env->CallStaticVoidMethod(
        cls.get(),
        getId(),
        detail::callToJni(detail::Convert<typename std::decay<Args>::type>::toCall(args))...);
  FACEBOOK_JNI_THROW_PENDING_EXCEPTION();
}

#pragma push_macro("DEFINE_PRIMITIVE_STATIC_CALL")
#undef DEFINE_PRIMITIVE_STATIC_CALL
#define DEFINE_PRIMITIVE_STATIC_CALL(TYPE, METHOD)                                             \
template<typename... Args>                                                                     \
inline TYPE JStaticMethod<TYPE(Args...)>::operator()(alias_ref<jclass> cls, Args... args) const { \
  const auto env = Environment::current();                                                         \
  auto result = env->CallStatic ## METHOD ## Method(                                           \
        cls.get(),                                                                             \
        getId(),                                                                               \
        detail::callToJni(detail::Convert<typename std::decay<Args>::type>::toCall(args))...); \
  FACEBOOK_JNI_THROW_PENDING_EXCEPTION();                                                      \
        return result;                                                                         \
}

DEFINE_PRIMITIVE_STATIC_CALL(jboolean, Boolean)
DEFINE_PRIMITIVE_STATIC_CALL(jbyte, Byte)
DEFINE_PRIMITIVE_STATIC_CALL(jchar, Char)
DEFINE_PRIMITIVE_STATIC_CALL(jshort, Short)
DEFINE_PRIMITIVE_STATIC_CALL(jint, Int)
DEFINE_PRIMITIVE_STATIC_CALL(jlong, Long)
DEFINE_PRIMITIVE_STATIC_CALL(jfloat, Float)
DEFINE_PRIMITIVE_STATIC_CALL(jdouble, Double)
#pragma pop_macro("DEFINE_PRIMITIVE_STATIC_CALL")

/// JStaticMethod specialization for references that wraps the return value in a @ref local_ref
template<typename R, typename... Args>
class JStaticMethod<R(Args...)> : public JMethodBase {

 public:
  using JniRet = typename detail::Convert<typename std::decay<R>::type>::jniType;
  static_assert(
      IsPlainJniReference<JniRet>() || detail::IsJavaClassType<JniRet>(),
      "Return type must be a JNI reference or JavaClass type.");
  using JMethodBase::JMethodBase;
  JStaticMethod() noexcept {};
  JStaticMethod(const JStaticMethod& other) noexcept = default;

  /// Invoke a method and return a local reference wrapping the result
  local_ref<JniRet> operator()(alias_ref<jclass> cls, Args... args) const {
    const auto env = Environment::current();
    auto result = env->CallStaticObjectMethod(
          cls.get(),
          getId(),
          detail::callToJni(detail::Convert<typename std::decay<Args>::type>::toCall(args))...);
    FACEBOOK_JNI_THROW_PENDING_EXCEPTION();
    return adopt_local(static_cast<JniType<JniRet>>(result));
  }

  friend class JClass;
};

template<typename... Args>
inline void
JNonvirtualMethod<void(Args...)>::operator()(alias_ref<jobject> self, alias_ref<jclass> cls, Args... args) const {
  const auto env = Environment::current();
  env->CallNonvirtualVoidMethod(
        self.get(),
        cls.get(),
        getId(),
        detail::callToJni(detail::Convert<typename std::decay<Args>::type>::toCall(args))...);
  FACEBOOK_JNI_THROW_PENDING_EXCEPTION();
}

#pragma push_macro("DEFINE_PRIMITIVE_NON_VIRTUAL_CALL")
#undef DEFINE_PRIMITIVE_NON_VIRTUAL_CALL
#define DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(TYPE, METHOD)                                                      \
template<typename... Args>                                                                                   \
inline TYPE                                                                                                  \
JNonvirtualMethod<TYPE(Args...)>::operator()(alias_ref<jobject> self, alias_ref<jclass> cls, Args... args) const { \
  const auto env = Environment::current();                                                                   \
  auto result = env->CallNonvirtual ## METHOD ## Method(                                                     \
        self.get(),                                                                                          \
        cls.get(),                                                                                           \
        getId(),                                                                                             \
        detail::callToJni(detail::Convert<typename std::decay<Args>::type>::toCall(args))...);               \
  FACEBOOK_JNI_THROW_PENDING_EXCEPTION();                                                                    \
  return result;                                                                                             \
}

DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(jboolean, Boolean)
DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(jbyte, Byte)
DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(jchar, Char)
DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(jshort, Short)
DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(jint, Int)
DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(jlong, Long)
DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(jfloat, Float)
DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(jdouble, Double)
#pragma pop_macro("DEFINE_PRIMITIVE_NON_VIRTUAL_CALL")

/// JNonvirtualMethod specialization for references that wraps the return value in a @ref local_ref
template<typename R, typename... Args>
class JNonvirtualMethod<R(Args...)> : public JMethodBase {
 public:
  using JniRet = typename detail::Convert<typename std::decay<R>::type>::jniType;
  static_assert(
      IsPlainJniReference<JniRet>() || detail::IsJavaClassType<JniRet>(),
      "Return type must be a JNI reference or JavaClass type.");
  using JMethodBase::JMethodBase;
  JNonvirtualMethod() noexcept {};
  JNonvirtualMethod(const JNonvirtualMethod& other) noexcept = default;

  /// Invoke a method and return a local reference wrapping the result
  local_ref<JniRet> operator()(alias_ref<jobject> self, alias_ref<jclass> cls, Args... args) const {
    const auto env = Environment::current();
    auto result = env->CallNonvirtualObjectMethod(
          self.get(),
          cls.get(),
          getId(),
          detail::callToJni(detail::Convert<typename std::decay<Args>::type>::toCall(args))...);
    FACEBOOK_JNI_THROW_PENDING_EXCEPTION();
    return adopt_local(static_cast<JniType<JniRet>>(result));
  }

  friend class JClass;
};

template <typename... Args>
local_ref<jobject> slowCall(jmethodID method_id, alias_ref<jobject> self, Args... args) {
    static auto invoke = findClassStatic("java/lang/reflect/Method")
      ->getMethod<jobject(jobject, JArrayClass<jobject>::javaobject)>("invoke");
    // TODO(xxxxxxx): Provide fbjni interface to ToReflectedMethod.
    auto reflected = adopt_local(Environment::current()->ToReflectedMethod(self->getClass().get(), method_id, JNI_FALSE));
    FACEBOOK_JNI_THROW_PENDING_EXCEPTION();
    if (!reflected) throw std::runtime_error("Unable to get reflected java.lang.reflect.Method");
    auto argsArray = makeArgsArray(args...);
    // No need to check for exceptions since invoke is itself a JMethod that will do that for us.
    return invoke(reflected, self.get(), argsArray.get());
}


// JField<T> ///////////////////////////////////////////////////////////////////////////////////////

template<typename T>
inline JField<T>::JField(jfieldID field) noexcept
  : field_id_{field}
{}

template<typename T>
inline JField<T>::operator bool() const noexcept {
  return field_id_ != nullptr;
}

template<typename T>
inline jfieldID JField<T>::getId() const noexcept {
  return field_id_;
}

#pragma push_macro("DEFINE_FIELD_PRIMITIVE_GET_SET")
#undef DEFINE_FIELD_PRIMITIVE_GET_SET
#define DEFINE_FIELD_PRIMITIVE_GET_SET(TYPE, METHOD)                 \
template<>                                                           \
inline TYPE JField<TYPE>::get(jobject object) const noexcept {       \
  const auto env = Environment::current();                           \
  return env->Get ## METHOD ## Field(object, field_id_);             \
}                                                                    \
                                                                     \
template<>                                                           \
inline void JField<TYPE>::set(jobject object, TYPE value) noexcept { \
  const auto env = Environment::current();                           \
  env->Set ## METHOD ## Field(object, field_id_, value);             \
}

DEFINE_FIELD_PRIMITIVE_GET_SET(jboolean, Boolean)
DEFINE_FIELD_PRIMITIVE_GET_SET(jbyte, Byte)
DEFINE_FIELD_PRIMITIVE_GET_SET(jchar, Char)
DEFINE_FIELD_PRIMITIVE_GET_SET(jshort, Short)
DEFINE_FIELD_PRIMITIVE_GET_SET(jint, Int)
DEFINE_FIELD_PRIMITIVE_GET_SET(jlong, Long)
DEFINE_FIELD_PRIMITIVE_GET_SET(jfloat, Float)
DEFINE_FIELD_PRIMITIVE_GET_SET(jdouble, Double)
#pragma pop_macro("DEFINE_FIELD_PRIMITIVE_GET_SET")

template<typename T>
inline T JField<T>::get(jobject object) const noexcept {
  return static_cast<T>(Environment::current()->GetObjectField(object, field_id_));
}

template<typename T>
inline void JField<T>::set(jobject object, T value) noexcept {
  Environment::current()->SetObjectField(object, field_id_, static_cast<jobject>(value));
}

// JStaticField<T> /////////////////////////////////////////////////////////////////////////////////

template<typename T>
inline JStaticField<T>::JStaticField(jfieldID field) noexcept
  : field_id_{field}
{}

template<typename T>
inline JStaticField<T>::operator bool() const noexcept {
  return field_id_ != nullptr;
}

template<typename T>
inline jfieldID JStaticField<T>::getId() const noexcept {
  return field_id_;
}

#pragma push_macro("DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET")
#undef DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET
#define DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(TYPE, METHOD)                \
template<>                                                                 \
inline TYPE JStaticField<TYPE>::get(jclass jcls) const noexcept {          \
  const auto env = Environment::current();                                 \
  return env->GetStatic ## METHOD ## Field(jcls, field_id_);               \
}                                                                          \
                                                                           \
template<>                                                                 \
inline void JStaticField<TYPE>::set(jclass jcls, TYPE value) noexcept {    \
  const auto env = Environment::current();                                 \
  env->SetStatic ## METHOD ## Field(jcls, field_id_, value);               \
}

DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(jboolean, Boolean)
DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(jbyte, Byte)
DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(jchar, Char)
DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(jshort, Short)
DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(jint, Int)
DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(jlong, Long)
DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(jfloat, Float)
DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(jdouble, Double)
#pragma pop_macro("DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET")

template<typename T>
inline T JStaticField<T>::get(jclass jcls) const noexcept {
  const auto env = Environment::current();
  return static_cast<T>(env->GetStaticObjectField(jcls, field_id_));
}

template<typename T>
inline void JStaticField<T>::set(jclass jcls, T value) noexcept {
  Environment::current()->SetStaticObjectField(jcls, field_id_, value);
}


// jmethod_traits //////////////////////////////////////////////////////////////////////////////////

// TODO(T6608405) Adapt this to implement a register natives method that requires no descriptor
namespace internal {

template<typename Head>
constexpr auto /* detail::SimpleFixedString<_> */ JavaDescriptor() {
  return jtype_traits<Head>::kDescriptor;
}

template<typename Head, typename Elem, typename... Tail>
constexpr auto /* detail::SimpleFixedString<_> */ JavaDescriptor() {
  return JavaDescriptor<Head>() + JavaDescriptor<Elem, Tail...>();
}

template<typename R, typename Arg1, typename... Args>
constexpr auto /* detail::SimpleFixedString<_> */ JMethodDescriptor() {
  return "(" + JavaDescriptor<Arg1, Args...>() + ")" + JavaDescriptor<R>();
}

template<typename R>
constexpr auto /* detail::SimpleFixedString<_> */  JMethodDescriptor() {
  return "()" + JavaDescriptor<R>();
}

} // internal
}}
