/*
 *  Copyright (c) 2000-2022 Inria
 *  All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions are met:
 *
 *  * Redistributions of source code must retain the above copyright notice,
 *  this list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright notice,
 *  this list of conditions and the following disclaimer in the documentation
 *  and/or other materials provided with the distribution.
 *  * Neither the name of the ALICE Project-Team nor the names of its
 *  contributors may be used to endorse or promote products derived from this
 *  software without specific prior written permission.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *  POSSIBILITY OF SUCH DAMAGE.
 *
 *  Contact: Bruno Levy
 *
 *     https://www.inria.fr/fr/bruno-levy
 *
 *     Inria,
 *     Domaine de Voluceau,
 *     78150 Le Chesnay - Rocquencourt
 *     FRANCE
 *
 */

#ifdef __ANDROID__

#include <geogram/basic/android_utils.h>
#include <geogram/basic/argused.h>
#include <geogram/basic/string.h>
#include <android_native_app_glue.h>

namespace {
    using namespace GEO;

/******************************************************************************/
    //Protocol to call a JNI function in an Android App:
    //https://groups.google.com/forum/?fromgroups=#!topic/android-ndk/Tk3g00wLKhk
    //   (see messages about how to attach/detach thread).

    /**
     * \brief To be called at the beginning of each function that uses the JNI.
     * \details Gets the JNIEnv and attaches the current thread to
     *  the JavaVM if need be.
     * \param[in] app a pointer to the android_app
     * \param[out] jni_env a pointer to the JNIEnv
     * \param[out] thread_attached true if this thread was attached to the VM,
     *  false otherwise.
     */
    inline void enter_JNI_function(
        android_app* app, JNIEnv*& jni_env, bool& thread_attached
    ) {
        jni_env = nullptr;
        thread_attached = false;
        JavaVM* java_vm = app->activity->vm;
        // Get JNIEnv from the JavaVM using GetEnv to test whether
        // thread is attached or not to the VM. If not, attach it
        // (and note that it will need to be detached at the end
        //  of the function).
        switch (java_vm->GetEnv((void**)&jni_env, JNI_VERSION_1_6)) {
        case JNI_OK:
            break;
        case JNI_EDETACHED: {
            jint attach_result =
                java_vm->AttachCurrentThread(&jni_env, nullptr);
            if(attach_result == JNI_ERR) {
                throw std::runtime_error("Could not attach current thread");
            }
            thread_attached = true;
        } break;
        case JNI_EVERSION:
            throw std::runtime_error("Invalid java version");
        }
    }

    /**
     * \brief To be called at the end of each function that uses the JNI.
     * \details Detaches the current thread from the JavaVM if need be.
     * \param[in] app a pointer to the android_app
     * \param[in] jni_env a pointer to the JNIEnv
     * \param[in] thread_attached true if this thread was attached to the VM
     *   by enter_JNI_function(), false otherwise.
     */
    inline void leave_JNI_function(
        android_app* app, JNIEnv* jni_env, bool thread_attached
    ) {
        geo_argused(jni_env);
        if(thread_attached) {
            app->activity->vm->DetachCurrentThread();
        }
    }

/******************************************************************************/
    // Display soft keyboard programmatically
    //https://groups.google.com/forum/?fromgroups=#!topic/android-ndk/Tk3g00wLKhk
    // There is normally an NDK function supposed to do that:
    // ANativeActivity_showSoftInput(
    //   mApplication->activity,ANATIVEACTIVITY_SHOW_SOFT_INPUT_FORCED
    // );
    // (or ANATIVEACTIVITY_SHOW_SOFT_INPUT_IMPLICIT)
    // but I did not manage to make it work....

    /**
     * \brief Shows or hides the soft keyboard.
     * \param[in] app a pointer to the android_app
     * \param[in) pShow true if the keyboard should be
     *  shown, false if it should be hidden.
     */
    void set_soft_keyboard_visibility(android_app* app, bool pShow) {
        JNIEnv* lJNIEnv = nullptr;
        bool thread_attached = false;
        enter_JNI_function(app, lJNIEnv, thread_attached);

        // Retrieves NativeActivity. Note the comment in native-activity.h:
        // "this member is mis-named. It should be named "activity" instead
        // of "clazz", since it's a reference to the NativeActivity instance
        // created by the system.
        jobject lNativeActivity = app->activity->clazz;
        jclass ClassNativeActivity = lJNIEnv->GetObjectClass(lNativeActivity);

        // Retrieves Context.INPUT_METHOD_SERVICE.
        jclass ClassContext = lJNIEnv->FindClass("android/content/Context");
        jfieldID FieldINPUT_METHOD_SERVICE =
            lJNIEnv->GetStaticFieldID(
                ClassContext,"INPUT_METHOD_SERVICE", "Ljava/lang/String;"
            );
        jobject INPUT_METHOD_SERVICE =
            lJNIEnv->GetStaticObjectField(
                ClassContext, FieldINPUT_METHOD_SERVICE
            );

        // Runs getSystemService(Context.INPUT_METHOD_SERVICE).
        jclass ClassInputMethodManager = lJNIEnv->FindClass(
            "android/view/inputmethod/InputMethodManager"
        );
        jmethodID MethodGetSystemService = lJNIEnv->GetMethodID(
            ClassNativeActivity, "getSystemService",
            "(Ljava/lang/String;)Ljava/lang/Object;"
        );
        jobject lInputMethodManager = lJNIEnv->CallObjectMethod(
            lNativeActivity, MethodGetSystemService,
            INPUT_METHOD_SERVICE
        );

        // Runs getWindow().getDecorView().
        jmethodID MethodGetWindow = lJNIEnv->GetMethodID(
            ClassNativeActivity, "getWindow",
            "()Landroid/view/Window;"
        );
        jobject lWindow = lJNIEnv->CallObjectMethod(
            lNativeActivity, MethodGetWindow
        );
        jclass ClassWindow = lJNIEnv->FindClass("android/view/Window");
        jmethodID MethodGetDecorView = lJNIEnv->GetMethodID(
            ClassWindow, "getDecorView", "()Landroid/view/View;"
        );
        jobject lDecorView = lJNIEnv->CallObjectMethod(
            lWindow, MethodGetDecorView
        );

        if (pShow) {
            // Runs lInputMethodManager.showSoftInput(...).
            jmethodID MethodShowSoftInput = lJNIEnv->GetMethodID(
                ClassInputMethodManager, "showSoftInput",
                "(Landroid/view/View;I)Z"
            );
            lJNIEnv->CallBooleanMethod(
                lInputMethodManager, MethodShowSoftInput,
                lDecorView, 0
            );
        } else {
            // Runs lWindow.getViewToken()
            jclass ClassView = lJNIEnv->FindClass(
                "android/view/View"
            );
            jmethodID MethodGetWindowToken = lJNIEnv->GetMethodID(
                ClassView, "getWindowToken", "()Landroid/os/IBinder;"
            );
            jobject lBinder = lJNIEnv->CallObjectMethod(
                lDecorView, MethodGetWindowToken
            );

            // lInputMethodManager.hideSoftInput(...).
            jmethodID MethodHideSoftInput = lJNIEnv->GetMethodID(
                ClassInputMethodManager, "hideSoftInputFromWindow",
                "(Landroid/os/IBinder;I)Z"
            );
            lJNIEnv->CallBooleanMethod(
                lInputMethodManager, MethodHideSoftInput,
                lBinder, 0
            );
        }

        leave_JNI_function(app, lJNIEnv, thread_attached);
    }

    /**
     * \brief Gets the internal name for an android permission.
     * \param[in] lJNIEnv a pointer to the JNI environment
     * \param[in] perm_name the name of the permission, e.g.,
     *   "READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE".
     * \return a jstring with the internal name of the permission,
     *   to be used with android Java functions
     *   Context.checkSelfPermission() or Activity.requestPermissions()
     */
    jstring android_permission_name(
        JNIEnv* lJNIEnv, const char* perm_name
    ) {
        // nested class permission in class android.Manifest,
        // hence android 'slash' Manifest 'dollar' permission
        jclass ClassManifestpermission = lJNIEnv->FindClass(
            "android/Manifest$permission"
        );

        jfieldID lid_PERM = lJNIEnv->GetStaticFieldID(
            ClassManifestpermission, perm_name, "Ljava/lang/String;"
        );

        return (jstring)(lJNIEnv->GetStaticObjectField(
                             ClassManifestpermission, lid_PERM
                         ));
    }
}

namespace GEO {

    namespace AndroidUtils {

        void show_soft_keyboard(android_app* app) {
            set_soft_keyboard_visibility(app, true);
        }

        void hide_soft_keyboard(android_app* app) {
            set_soft_keyboard_visibility(app, false);
        }

        Numeric::int32 keycode_to_unicode(
            android_app* app,
            Numeric::int32 pDeviceId,
            Numeric::int32 pKeyCode,
            Numeric::int32 pMetaState
        ) {
            jint result = 0;

            // Early exit for special keys
            // (works without it, but well, why calling all that
            //  Java stuff if we now in advance that we do not need
            //  to ?).
            if(
                pKeyCode == AKEYCODE_TAB ||
                pKeyCode == AKEYCODE_DPAD_LEFT ||
                pKeyCode == AKEYCODE_DPAD_RIGHT ||
                pKeyCode == AKEYCODE_DPAD_UP ||
                pKeyCode == AKEYCODE_DPAD_DOWN ||
                pKeyCode == AKEYCODE_PAGE_UP ||
                pKeyCode == AKEYCODE_PAGE_DOWN ||
                pKeyCode == AKEYCODE_MOVE_HOME ||
                pKeyCode == AKEYCODE_MOVE_END ||
                pKeyCode == AKEYCODE_INSERT ||
                pKeyCode == AKEYCODE_FORWARD_DEL ||
                pKeyCode == AKEYCODE_DEL ||
                pKeyCode == AKEYCODE_ENTER ||
                pKeyCode == AKEYCODE_ESCAPE
            ) {
                return result;
            }

            JNIEnv* lJNIEnv = nullptr;
            bool thread_attached = false;
            enter_JNI_function(app, lJNIEnv, thread_attached);

            jclass ClassKeyCharacterMap = lJNIEnv->FindClass(
                "android/view/KeyCharacterMap"
            );

            jmethodID MethodLoad = lJNIEnv->GetStaticMethodID(
                ClassKeyCharacterMap, "load",
                "(I)Landroid/view/KeyCharacterMap;"
            );

            jobject lKeyCharacterMap = lJNIEnv->CallStaticObjectMethod(
                ClassKeyCharacterMap, MethodLoad, jint(pDeviceId)
            );

            jmethodID MethodGet = lJNIEnv->GetMethodID(
                ClassKeyCharacterMap, "get",
                "(II)I"
            );

            result = lJNIEnv->CallIntMethod(
                lKeyCharacterMap, MethodGet,
                jint(pKeyCode), jint(pMetaState)
            );

            leave_JNI_function(app, lJNIEnv, thread_attached);
            return result;
        }

        bool has_permission(android_app* app, const char* perm) {
            JNIEnv* lJNIEnv = nullptr;
            bool thread_attached = false;
            enter_JNI_function(app, lJNIEnv, thread_attached);

            // Get the symbolic value PERMISSION_GRANTED
            jclass ClassPackageManager = lJNIEnv->FindClass(
                "android/content/pm/PackageManager"
            );
            jfieldID lid_PERMISSION_GRANTED = lJNIEnv->GetStaticFieldID(
                ClassPackageManager, "PERMISSION_GRANTED", "I"
            );
            jint PERMISSION_GRANTED = lJNIEnv->GetStaticIntField(
                ClassPackageManager, lid_PERMISSION_GRANTED
            );

            // Call checkSelfPermission

            // Retrieves NativeActivity. Note the comment in native-activity.h:
            // "this member is mis-named. It should be named "activity" instead
            // of "clazz", since it's a reference to the NativeActivity instance
            // created by the system.
            jobject activity = app->activity->clazz;

            jclass ClassContext = lJNIEnv->FindClass(
                "android/content/Context"
            );
            jmethodID MethodcheckSelfPermission = lJNIEnv->GetMethodID(
                ClassContext, "checkSelfPermission", "(Ljava/lang/String;)I"
            );
            jint int_result = lJNIEnv->CallIntMethod(
                activity, MethodcheckSelfPermission,
                android_permission_name(lJNIEnv, perm)
            );
            bool result = (int_result == PERMISSION_GRANTED);

            leave_JNI_function(app, lJNIEnv, thread_attached);
            return result;
        }

        void request_permissions(
            android_app* app, int nb_perms, const char** perms
        ) {
            JNIEnv* lJNIEnv = nullptr;
            bool thread_attached = false;
            enter_JNI_function(app, lJNIEnv, thread_attached);

            jobjectArray perm_array = lJNIEnv->NewObjectArray(
                nb_perms,
                lJNIEnv->FindClass("java/lang/String"),
                lJNIEnv->NewStringUTF("")
            );

            for(int i=0; i<nb_perms; ++i) {
                lJNIEnv->SetObjectArrayElement(
                    perm_array, i,
                    android_permission_name(lJNIEnv, perms[i])
                );
            }

            // Retrieves NativeActivity. Note the comment in native-activity.h:
            // "this member is mis-named. It should be named "activity" instead
            // of "clazz", since it's a reference to the NativeActivity instance
            // created by the system.
            jobject activity = app->activity->clazz;

            jclass ClassActivity = lJNIEnv->FindClass(
                "android/app/Activity"
            );

            jmethodID MethodrequestPermissions = lJNIEnv->GetMethodID(
                ClassActivity, "requestPermissions", "([Ljava/lang/String;I)V"
            );

            // Last arg (0) is just for the callback (that I do not use)
            lJNIEnv->CallVoidMethod(
                activity, MethodrequestPermissions, perm_array, 0
            );

            leave_JNI_function(app, lJNIEnv, thread_attached);
        }


        // See https://stackoverflow.com/questions/7595324/creating-temporary-files-in-android-with-ndk/10334111
        std::string temp_folder(android_app* app) {
            JNIEnv* env = nullptr;
            bool thread_attached = false;
            enter_JNI_function(app, env, thread_attached);

            jclass activityClass = env->FindClass(
                "android/app/NativeActivity"
            );
            jmethodID getCacheDir = env->GetMethodID(
                activityClass, "getCacheDir", "()Ljava/io/File;"
            );

            // Note the comment in native-activity.h:
            // "this member is mis-named. It should be named "activity" instead
            // of "clazz", since it's a reference to the NativeActivity instance
            // created by the system.
            jobject cache_dir = env->CallObjectMethod(
                app->activity->clazz, getCacheDir
            );

            jclass fileClass = env->FindClass("java/io/File");
            jmethodID getPath = env->GetMethodID(
                fileClass, "getPath", "()Ljava/lang/String;"
            );
            jstring path_string = (jstring)env->CallObjectMethod(
                cache_dir, getPath
            );

            const char *path_chars = env->GetStringUTFChars(
                path_string,nullptr
            );
            std::string result(path_chars);

            leave_JNI_function(app, env, thread_attached);
            return result;
        }
    }
}

/*******************************************************************/

namespace {
    using namespace GEO;

    static const char* event_type_to_str(int32_t event_type) {
        switch(event_type) {
        case AINPUT_EVENT_TYPE_KEY:
            return "key";
        case AINPUT_EVENT_TYPE_MOTION:
            return "motion";
        default:
            return "unknown";
        }
    }

    static const char* event_action_to_str(int32_t event_action) {
        switch(event_action) {
        case AKEY_EVENT_ACTION_DOWN:
            return "key/motion_down";
        case AKEY_EVENT_ACTION_UP:
            return "key/motion_up";
        case AMOTION_EVENT_ACTION_BUTTON_PRESS:
            return "motion_button_press";
        case AMOTION_EVENT_ACTION_BUTTON_RELEASE:
            return "motion_button_release";
        case AMOTION_EVENT_ACTION_HOVER_MOVE:
            return "motion_hover_move";
        case AMOTION_EVENT_ACTION_MOVE:
            return "motion_move";
        case AMOTION_EVENT_ACTION_SCROLL:
            return "motion_scroll";
        default:
            return "unknown";
        }
    }

    static const char* event_tool_type_to_str(int32_t event_tool_type) {
        switch(event_tool_type) {
        case AMOTION_EVENT_TOOL_TYPE_MOUSE:
            return "mouse";
        case AMOTION_EVENT_TOOL_TYPE_STYLUS:
            return "stylus";
        case AMOTION_EVENT_TOOL_TYPE_ERASER:
            return "eraser";
        case AMOTION_EVENT_TOOL_TYPE_FINGER:
            return "finger";
        default:
            return "unknown";
        }
    }
}

namespace GEO {
    namespace AndroidUtils {

        void debug_show_event(AInputEvent* event) {
            std::string msg = std::string("Event=") + " type:"   +
                std::string(event_type_to_str(AInputEvent_getType(event)));

            if(AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION) {
                msg += " action:" + std::string(
                    event_action_to_str(AMotionEvent_getAction(event))
                ) + " tool:" + std::string(
                    event_tool_type_to_str(AMotionEvent_getToolType(event,0))
                ) + " nb_pointers:" + ::GEO::String::to_string(
                    int(AMotionEvent_getPointerCount(event))
                ) ;
            }
            ::GEO::AndroidUtils::debug_log(msg);
        }
    }
}

#endif
