/*-------------------------------------------------------------------------
 * drawElements Quality Program Tester Core
 * ----------------------------------------
 *
 * Copyright 2014 The Android Open Source Project
 *
 * 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
 * \brief Android utilities.
 *//*--------------------------------------------------------------------*/

#include "tcuAndroidUtil.hpp"

#include "deSTLUtil.hpp"
#include "deMath.h"

#include <vector>

namespace tcu
{
namespace Android
{

using std::string;
using std::vector;

namespace
{

class ScopedJNIEnv
{
public:
    ScopedJNIEnv(JavaVM *vm);
    ~ScopedJNIEnv(void);

    JavaVM *getVM(void) const
    {
        return m_vm;
    }
    JNIEnv *getEnv(void) const
    {
        return m_env;
    }

private:
    JavaVM *const m_vm;
    JNIEnv *m_env;
    bool m_detach;
};

ScopedJNIEnv::ScopedJNIEnv(JavaVM *vm) : m_vm(vm), m_env(DE_NULL), m_detach(false)
{
    const int getEnvRes = m_vm->GetEnv((void **)&m_env, JNI_VERSION_1_6);

    if (getEnvRes == JNI_EDETACHED)
    {
        if (m_vm->AttachCurrentThread(&m_env, DE_NULL) != JNI_OK)
            throw std::runtime_error("JNI AttachCurrentThread() failed");

        m_detach = true;
    }
    else if (getEnvRes != JNI_OK)
        throw std::runtime_error("JNI GetEnv() failed");

    DE_ASSERT(m_env);
}

ScopedJNIEnv::~ScopedJNIEnv(void)
{
    if (m_detach)
        m_vm->DetachCurrentThread();
}

class LocalRef
{
public:
    LocalRef(JNIEnv *env, jobject ref);
    ~LocalRef(void);

    jobject operator*(void) const
    {
        return m_ref;
    }
    operator bool(void) const
    {
        return !!m_ref;
    }

private:
    LocalRef(const LocalRef &);
    LocalRef &operator=(const LocalRef &);

    JNIEnv *const m_env;
    const jobject m_ref;
};

LocalRef::LocalRef(JNIEnv *env, jobject ref) : m_env(env), m_ref(ref)
{
}

LocalRef::~LocalRef(void)
{
    if (m_ref)
        m_env->DeleteLocalRef(m_ref);
}

void checkException(JNIEnv *env)
{
    if (env->ExceptionCheck())
    {
        env->ExceptionDescribe();
        env->ExceptionClear();
        throw std::runtime_error("Got JNI exception");
    }
}

jclass findClass(JNIEnv *env, const char *className)
{
    const jclass cls = env->FindClass(className);

    checkException(env);
    TCU_CHECK_INTERNAL(cls);

    return cls;
}

jclass getObjectClass(JNIEnv *env, jobject object)
{
    const jclass cls = env->GetObjectClass(object);

    checkException(env);
    TCU_CHECK_INTERNAL(cls);

    return cls;
}

jmethodID getMethodID(JNIEnv *env, jclass cls, const char *methodName, const char *signature)
{
    const jmethodID id = env->GetMethodID(cls, methodName, signature);

    checkException(env);
    TCU_CHECK_INTERNAL(id);

    return id;
}

string getStringValue(JNIEnv *env, jstring jniStr)
{
    const char *ptr  = env->GetStringUTFChars(jniStr, DE_NULL);
    const string str = string(ptr);

    env->ReleaseStringUTFChars(jniStr, ptr);

    return str;
}

string getIntentStringExtra(JNIEnv *env, jobject activity, const char *name)
{
    // \todo [2013-05-12 pyry] Clean up references on error.

    const jclass activityCls = getObjectClass(env, activity);
    const LocalRef intent(
        env, env->CallObjectMethod(activity, getMethodID(env, activityCls, "getIntent", "()Landroid/content/Intent;")));
    TCU_CHECK_INTERNAL(intent);

    const LocalRef extraName(env, env->NewStringUTF(name));
    const jclass intentCls = getObjectClass(env, *intent);
    TCU_CHECK_INTERNAL(extraName && intentCls);

    jvalue getExtraArgs[1];
    getExtraArgs[0].l = *extraName;

    const LocalRef extraStr(env, env->CallObjectMethodA(*intent,
                                                        getMethodID(env, intentCls, "getStringExtra",
                                                                    "(Ljava/lang/String;)Ljava/lang/String;"),
                                                        getExtraArgs));

    if (extraStr)
        return getStringValue(env, (jstring)*extraStr);
    else
        return string();
}

void setRequestedOrientation(JNIEnv *env, jobject activity, ScreenOrientation orientation)
{
    const jclass activityCls         = getObjectClass(env, activity);
    const jmethodID setOrientationId = getMethodID(env, activityCls, "setRequestedOrientation", "(I)V");

    env->CallVoidMethod(activity, setOrientationId, (int)orientation);
}

template <typename Type>
const char *getJNITypeStr(void);

template <>
const char *getJNITypeStr<int>(void)
{
    return "I";
}

template <>
const char *getJNITypeStr<int64_t>(void)
{
    return "J";
}

template <>
const char *getJNITypeStr<string>(void)
{
    return "Ljava/lang/String;";
}

template <>
const char *getJNITypeStr<vector<string>>(void)
{
    return "[Ljava/lang/String;";
}

template <typename FieldType>
FieldType getStaticFieldValue(JNIEnv *env, jclass cls, jfieldID fieldId);

template <>
int getStaticFieldValue<int>(JNIEnv *env, jclass cls, jfieldID fieldId)
{
    DE_ASSERT(cls && fieldId);
    return env->GetStaticIntField(cls, fieldId);
}

template <>
string getStaticFieldValue<string>(JNIEnv *env, jclass cls, jfieldID fieldId)
{
    const jstring jniStr = (jstring)env->GetStaticObjectField(cls, fieldId);

    if (jniStr)
        return getStringValue(env, jniStr);
    else
        return string();
}

template <>
vector<string> getStaticFieldValue<vector<string>>(JNIEnv *env, jclass cls, jfieldID fieldId)
{
    const jobjectArray array = (jobjectArray)env->GetStaticObjectField(cls, fieldId);
    vector<string> result;

    checkException(env);

    if (array)
    {
        const int numElements = env->GetArrayLength(array);

        for (int ndx = 0; ndx < numElements; ndx++)
        {
            const jstring jniStr = (jstring)env->GetObjectArrayElement(array, ndx);

            checkException(env);

            if (jniStr)
                result.push_back(getStringValue(env, jniStr));
        }
    }

    return result;
}

template <typename FieldType>
FieldType getStaticField(JNIEnv *env, const char *className, const char *fieldName)
{
    const jclass cls       = findClass(env, className);
    const jfieldID fieldId = env->GetStaticFieldID(cls, fieldName, getJNITypeStr<FieldType>());

    checkException(env);

    if (fieldId)
        return getStaticFieldValue<FieldType>(env, cls, fieldId);
    else
        throw std::runtime_error(string(fieldName) + " not found in " + className);
}

template <typename FieldType>
FieldType getFieldValue(JNIEnv *env, jobject obj, jfieldID fieldId);

template <>
int64_t getFieldValue<int64_t>(JNIEnv *env, jobject obj, jfieldID fieldId)
{
    DE_ASSERT(obj && fieldId);
    return env->GetLongField(obj, fieldId);
}

template <typename FieldType>
FieldType getField(JNIEnv *env, jobject obj, const char *fieldName)
{
    const jclass cls       = getObjectClass(env, obj);
    const jfieldID fieldId = env->GetFieldID(cls, fieldName, getJNITypeStr<FieldType>());

    checkException(env);

    if (fieldId)
        return getFieldValue<FieldType>(env, obj, fieldId);
    else
        throw std::runtime_error(string(fieldName) + " not found in object");
}

void describePlatform(JNIEnv *env, std::ostream &dst)
{
    const char *const buildClass   = "android/os/Build";
    const char *const versionClass = "android/os/Build$VERSION";

    static const struct
    {
        const char *classPath;
        const char *className;
        const char *fieldName;
    } s_stringFields[] = {
        {buildClass, "Build", "BOARD"},        {buildClass, "Build", "BRAND"},
        {buildClass, "Build", "DEVICE"},       {buildClass, "Build", "DISPLAY"},
        {buildClass, "Build", "FINGERPRINT"},  {buildClass, "Build", "HARDWARE"},
        {buildClass, "Build", "MANUFACTURER"}, {buildClass, "Build", "MODEL"},
        {buildClass, "Build", "PRODUCT"},      {buildClass, "Build", "TAGS"},
        {buildClass, "Build", "TYPE"},         {versionClass, "Build.VERSION", "RELEASE"},
    };

    for (int ndx = 0; ndx < DE_LENGTH_OF_ARRAY(s_stringFields); ndx++)
        dst << s_stringFields[ndx].className << "." << s_stringFields[ndx].fieldName << ": "
            << getStaticField<string>(env, s_stringFields[ndx].classPath, s_stringFields[ndx].fieldName) << "\n";

    dst << "Build.VERSION.SDK_INT: " << getStaticField<int>(env, versionClass, "SDK_INT") << "\n";

    {
        const vector<string> supportedAbis = getStaticField<vector<string>>(env, buildClass, "SUPPORTED_ABIS");

        dst << "Build.SUPPORTED_ABIS: ";

        for (size_t ndx = 0; ndx < supportedAbis.size(); ndx++)
            dst << (ndx != 0 ? ", " : "") << supportedAbis[ndx];

        dst << "\n";
    }
}

} // namespace

ScreenOrientation mapScreenRotation(ScreenRotation rotation)
{
    switch (rotation)
    {
    case SCREENROTATION_UNSPECIFIED:
        return SCREEN_ORIENTATION_UNSPECIFIED;
    case SCREENROTATION_0:
        return SCREEN_ORIENTATION_PORTRAIT;
    case SCREENROTATION_90:
        return SCREEN_ORIENTATION_LANDSCAPE;
    case SCREENROTATION_180:
        return SCREEN_ORIENTATION_REVERSE_PORTRAIT;
    case SCREENROTATION_270:
        return SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
    default:
        print("Warning: Unsupported rotation");
        return SCREEN_ORIENTATION_PORTRAIT;
    }
}

string getIntentStringExtra(ANativeActivity *activity, const char *name)
{
    const ScopedJNIEnv env(activity->vm);

    return getIntentStringExtra(env.getEnv(), activity->clazz, name);
}

void setRequestedOrientation(ANativeActivity *activity, ScreenOrientation orientation)
{
    const ScopedJNIEnv env(activity->vm);

    setRequestedOrientation(env.getEnv(), activity->clazz, orientation);
}

void describePlatform(ANativeActivity *activity, std::ostream &dst)
{
    const ScopedJNIEnv env(activity->vm);

    describePlatform(env.getEnv(), dst);
}

size_t getTotalAndroidSystemMemory(ANativeActivity *activity)
{
    const ScopedJNIEnv scopedJniEnv(activity->vm);
    JNIEnv *env = scopedJniEnv.getEnv();

    // Get activity manager instance:
    // ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    const jclass activityManagerClass = findClass(env, "android/app/ActivityManager");
    const LocalRef activityString(env, env->NewStringUTF("activity")); // Context.ACTIVITY_SERVICE == "activity"
    const jclass activityClass = getObjectClass(env, activity->clazz);
    const jmethodID getServiceID =
        getMethodID(env, activityClass, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;");
    LocalRef activityManager(env, env->CallObjectMethod(activity->clazz, getServiceID, *activityString));
    checkException(env);
    TCU_CHECK_INTERNAL(activityManager);

    // Crete memory info instance:
    // ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
    const jclass memoryInfoClass   = findClass(env, "android/app/ActivityManager$MemoryInfo");
    const jmethodID memoryInfoCtor = getMethodID(env, memoryInfoClass, "<init>", "()V");
    LocalRef memoryInfo(env, env->NewObject(memoryInfoClass, memoryInfoCtor));
    checkException(env);
    TCU_CHECK_INTERNAL(memoryInfo);

    // Get memory info from activity manager:
    // activityManager.getMemoryInfo(memoryInfo);
    const jmethodID getMemoryInfoID =
        getMethodID(env, activityManagerClass, "getMemoryInfo", "(Landroid/app/ActivityManager$MemoryInfo;)V");
    checkException(env);
    env->CallVoidMethod(*activityManager, getMemoryInfoID, *memoryInfo);

    // Return 'totalMem' field from the memory info instance.
    return static_cast<size_t>(getField<int64_t>(env, *memoryInfo, "totalMem"));
}

} // namespace Android
} // namespace tcu
