/*
 *
 *    Copyright (c) 2021 Project CHIP Authors
 *
 *    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 <lib/core/CHIPError.h>
#include <lib/support/CodeUtils.h>
#include <lib/support/Span.h>
#include <lib/support/TypeTraits.h>
#include <string>

#define JNI_LOCAL_REF_COUNT 256

namespace chip {
class JniLocalReferenceScope
{
public:
    explicit JniLocalReferenceScope(JNIEnv * env) : mEnv(env)
    {
        if (mEnv->PushLocalFrame(JNI_LOCAL_REF_COUNT) == 0)
        {
            mlocalFramePushed = true;
        }
    }

    ~JniLocalReferenceScope()
    {
        if (mlocalFramePushed)
        {
            mEnv->PopLocalFrame(nullptr);
            mlocalFramePushed = false;
        }
    }

    // Delete copy constructor and copy assignment operator
    JniLocalReferenceScope(const JniLocalReferenceScope &)             = delete;
    JniLocalReferenceScope & operator=(const JniLocalReferenceScope &) = delete;

private:
    JNIEnv * const mEnv;
    bool mlocalFramePushed = false;
};

class JniGlobalReference
{
public:
    JniGlobalReference() {}

    CHIP_ERROR Init(jobject aObjectRef);

    JniGlobalReference(JniGlobalReference && aOther)
    {
        mObjectRef        = aOther.mObjectRef;
        aOther.mObjectRef = nullptr;
    }

    ~JniGlobalReference() { Reset(); }

    void Reset();

    jobject ObjectRef() const { return mObjectRef; }

    bool HasValidObjectRef() const { return mObjectRef != nullptr; }

private:
    jobject mObjectRef = nullptr;
};

class JniReferences
{
public:
    // No copy, move or assignment.
    JniReferences(const JniReferences &)             = delete;
    JniReferences(const JniReferences &&)            = delete;
    JniReferences & operator=(const JniReferences &) = delete;

    static JniReferences & GetInstance()
    {
        static JniReferences jniReferences;
        return jniReferences;
    }

    /**
     * Set the JavaVM.
     *
     * we need clsType in context to get ClassLoader
     *
     * This must be called before GetEnvForCurrentThread().
     */
    void SetJavaVm(JavaVM * jvm, const char * clsType);

    /**
     * Returns a JNIEnv for the current thread.
     *
     * This must be called after SetJavaVm(). If the current thread is not attached to the JVM, this method will attach the thread
     * first, then retrieve the JNIEnv.
     */
    JNIEnv * GetEnvForCurrentThread();

    /**
     * @brief
     *   Creates a local jclass reference to the given class type.
     *
     *   This must be called after SetJavaVm().
     *
     * @param[in] env The JNIEnv for finding a Java class and creating a new Java reference.
     * @param[in] clsType The fully-qualified Java class name to find, e.g. java/lang/IllegalStateException.
     * @param[out] outCls A Java reference to the class matching clsType.
     */
    CHIP_ERROR GetLocalClassRef(JNIEnv * env, const char * clsType, jclass & outCls);

    CHIP_ERROR FindMethod(JNIEnv * env, jobject object, const char * methodName, const char * methodSignature,
                          jmethodID * methodId);

    CHIP_ERROR FindMethod(JNIEnv * env, jclass javaClass, const char * methodName, const char * methodSignature,
                          jmethodID * methodId);

    void CallVoidInt(JNIEnv * env, jobject object, const char * methodName, jint argument);

    void CallVoidLong(JNIEnv * env, jobject object, const char * methodName, jlong argument);

    CHIP_ERROR N2J_ByteArray(JNIEnv * env, const uint8_t * inArray, jsize inArrayLen, jbyteArray & outArray);

    void ReportError(JNIEnv * env, CHIP_ERROR cbErr, const char * functName);

    void ThrowError(JNIEnv * env, jclass exceptionCls, CHIP_ERROR errToThrow);

    void ThrowError(JNIEnv * env, JniGlobalReference & exceptionCls, CHIP_ERROR errToThrow);
    /**
     * Creates a java.util.Optional wrapping the specified jobject. If the wrapped jobject is null, an empty
     * Optional will be returned.
     */
    CHIP_ERROR CreateOptional(jobject objectToWrap, jobject & outOptional);

    /**
     * Retrieve the value of a java.util.Optional, or nullptr if the Optional is empty.
     */
    CHIP_ERROR GetOptionalValue(jobject optionalObj, jobject & optionalValue);

    /**
     * Get a primitive jint from the Java boxed type Integer, using intValue().
     */
    jint IntegerToPrimitive(jobject boxedObject);

    /**
     * Get a primitive jlong from the Java boxed type Long, using longValue().
     */
    jlong LongToPrimitive(jobject boxedObject);

    /**
     * Get a primitive jboolean from the Java boxed type Booelan, using booleanValue().
     */
    jboolean BooleanToPrimitive(jobject boxedObject);

    /**
     * Get a primitive jfloat from the Java boxed type Float, using floatValue().
     */
    jfloat FloatToPrimitive(jobject boxedObject);

    /**
     * Get a primitive jfloat from the Java boxed type Double, using doubleValue().
     */
    jdouble DoubleToPrimitive(jobject boxedObject);

    /**
     * Get a primitive jshort from the Java boxed type Short, using shortValue().
     */
    jshort ShortToPrimitive(jobject boxedShort);

    CHIP_ERROR CreateArrayList(jobject & outList);

    CHIP_ERROR AddToList(jobject list, jobject objectToAdd);

    CHIP_ERROR GetListSize(jobject list, jint & size);

    CHIP_ERROR GetListItem(jobject list, jint index, jobject & outItem);

    CHIP_ERROR CreateHashMap(jobject & outMap);

    CHIP_ERROR PutInMap(jobject map, jobject key, jobject value);

    CHIP_ERROR GetObjectField(jobject objectToRead, const char * name, const char * signature, jobject & outObject);

    /**
     * Call a void method with subscriptionId named "OnSubscriptionEstablished" on the provided jobject.
     */
    CHIP_ERROR CallSubscriptionEstablished(jobject javaCallback, long subscriptionId);

    /**
     * Creates a boxed type (e.g. java.lang.Integer) based on the the class name ("java/lang/Integer"), constructor JNI signature
     * ("(I)V"), and value.
     */
    template <class T, typename std::enable_if_t<!std::is_enum<T>::value, int> = 0>
    CHIP_ERROR CreateBoxedObject(std::string boxedTypeClsName, std::string constructorSignature, T value, jobject & outObj)
    {
        JNIEnv * env = GetEnvForCurrentThread();
        VerifyOrReturnError(env != nullptr, CHIP_ERROR_INCORRECT_STATE);
        jclass boxedTypeCls = nullptr;
        ReturnErrorOnFailure(GetLocalClassRef(env, boxedTypeClsName.c_str(), boxedTypeCls));

        jmethodID boxedTypeConstructor = env->GetMethodID(boxedTypeCls, "<init>", constructorSignature.c_str());
        outObj                         = env->NewObject(boxedTypeCls, boxedTypeConstructor, value);
        return CHIP_NO_ERROR;
    }

    /**
     * Handling for strongly-typed enums.
     */
    template <class T, typename std::enable_if_t<std::is_enum<T>::value, int> = 0>
    CHIP_ERROR CreateBoxedObject(std::string boxedTypeClsName, std::string constructorSignature, T value, jobject & outObj)
    {
        return CreateBoxedObject(boxedTypeClsName, constructorSignature, chip::to_underlying(value), outObj);
    }

    /**
     * Use instead of 'NewStringUTF' function
     * If the value is not decoded with "UTF-8", the error will be returned.
     * (The NewStringUTF function crashes when the value can not decoded as "UTF-8".)
     *
     * Creates a java string type based on char array.
     */
    CHIP_ERROR CharToStringUTF(const chip::CharSpan & charSpan, jobject & outString);

private:
    JniReferences() {}

    JavaVM * mJvm = nullptr;
    JniGlobalReference mClassLoader;
    jmethodID mFindClassMethod = nullptr;

    JniGlobalReference mHashMapClass;
    JniGlobalReference mListClass;
    JniGlobalReference mArrayListClass;
};

} // namespace chip
