/*----------------------------------------------------------------------------
 ChucK Concurrent, On-the-fly Audio Programming Language
 Compiler and Virtual Machine

 Copyright (c) 2004 Ge Wang and Perry R. Cook.  All rights reserved.
   http://chuck.stanford.edu/
   http://chuck.cs.princeton.edu/

 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 U.S.A.
 -----------------------------------------------------------------------------*/

//-----------------------------------------------------------------------------
// name: util_platforms.cpp
// desc: platform-specific utilities, e.g., tmpfile() for Android etc.
//
// author: Andriy Kunitsyn (kunitzin@gmail.com) original ChuckAndroid
//         Ge Wang (https://ccrma.stanford.edu/~ge/) added util_platforms.*
// date: Summer 2021
//-----------------------------------------------------------------------------
#include "util_platforms.h"




//-----------------------------------------------------------------------------
#ifdef __ANDROID__
//-----------------------------------------------------------------------------
#include <jni.h>
#include <fcntl.h>
#include <unistd.h>
#include <string>

static JavaVM * jvm_instance = NULL;

JNIEXPORT jint JNICALL JNI_OnLoad( JavaVM * vm, void * reserved )
{
    jvm_instance = vm;

    return JNI_VERSION_1_6;
}

JavaVM * ChuckAndroid::getJVM()
{
    return jvm_instance;
}

//-----------------------------------------------------------------------------
// name: class JNIEnvWrapper
// desc: Wrapper around JNIEnv. Serves three purposes:
//       - releases JNIEnv and Java objects in RAII fashion
//       - removes the need to get method IDs
//       - throws JNIEnvWrapperException exception on Java exceptions
//-----------------------------------------------------------------------------
class JNIEnvWrapper
{
public:
    JNIEnvWrapper(JavaVM * jvm);
    ~JNIEnvWrapper();

    bool IsValid() const;

    jclass FindClass(const char * class_signature);
    jobject ConstructNewObject(jclass constructed_class,
        const char * constructor_signature, jobject param_1);
    jobject CallObjectMethod(jobject callee, jclass callee_class,
        const char * method_name, const char * method_signature);
    jobject CallObjectMethod(jobject callee, jclass callee_class,
        const char * method_name, const char * method_signature,
        jobject param_1);
    jobject CallStaticObjectMethod(jclass callee_class,
        const char * method_name, const char * method_signature);
    jobject CallStaticObjectMethod(jclass callee_class,
        const char * method_name, const char * method_signature,
        jobject param_1);
    jobject CallStaticObjectMethod(jclass callee_class,
        const char * method_name, const char * method_signature,
        jobject param_1, jobject param_2, jobject param_3);
    jint CallIntMethod(jobject callee, jclass callee_class,
        const char * method_name, const char * method_signature,
        jobject param_1);
    void CallVoidMethod(jobject callee, jclass callee_class,
        const char * method_name, const char * method_signature);

    jstring NewStringUTF(const char * utf8_string);
    const char * GetStringUTFChars(jstring string);
    void ReleaseStringUTFChars(jstring string, const char * utf8_chars);
    jbyteArray NewByteArray(size_t size);
    jbyte * GetByteArrayElements(jbyteArray java_array);
    void ReleaseByteArrayElements(jbyteArray java_array, jbyte * native_array);

private:
    void ThrowOnJavaException();
    JavaVM * m_jvm = NULL;
    JNIEnv * m_env = NULL;
    bool m_needs_detaching = false;
};

class JNIEnvWrapperException {};

JNIEnvWrapper::JNIEnvWrapper(JavaVM * jvm)
    : m_jvm(jvm)
{
    int reason = jvm->GetEnv((void **)&m_env, JNI_VERSION_1_6);
    if( reason != JNI_OK && reason != JNI_EDETACHED )
    {
        m_env = NULL;
        return;
    }
    if( reason == JNI_EDETACHED )
    {
        if( jvm->AttachCurrentThread(&m_env, NULL) != 0 )
        {
            m_env = NULL;
            return;
        }
        m_needs_detaching = true;
    }
    if( m_env->PushLocalFrame(32) != 0 )
    {
        if( m_needs_detaching )
        {
            m_jvm->DetachCurrentThread();
        }
        m_env = NULL;
    }
}

JNIEnvWrapper::~JNIEnvWrapper()
{
    if( m_env == NULL )
    {
        return;
    }
    m_env->PopLocalFrame(NULL);
    if( m_needs_detaching )
    {
        m_jvm->DetachCurrentThread();
    }
}

bool JNIEnvWrapper::IsValid() const
{
    return m_env != NULL;
}

void JNIEnvWrapper::ThrowOnJavaException()
{
    if( m_env->ExceptionCheck() )
    {
        m_env->ExceptionDescribe();
        m_env->ExceptionClear();
        throw JNIEnvWrapperException();
    }
}

jclass JNIEnvWrapper::FindClass(const char * class_signature)
{
    jclass ret = m_env->FindClass(class_signature);
    ThrowOnJavaException();
    return ret;
}

jobject JNIEnvWrapper::ConstructNewObject(jclass constructed_class,
    const char * constructor_signature, jobject param_1)
{
    jmethodID method_id = m_env->GetMethodID(constructed_class,
        "<init>", constructor_signature);
    ThrowOnJavaException();
    jobject ret = m_env->NewObject(constructed_class, method_id,
        param_1);
    ThrowOnJavaException();
    return ret;
}

jobject JNIEnvWrapper::CallObjectMethod(jobject callee, jclass callee_class,
    const char * method_name, const char * method_signature)
{
    jmethodID method_id = m_env->GetMethodID(callee_class,
        method_name, method_signature);
    ThrowOnJavaException();
    jobject ret = m_env->CallObjectMethod(callee, method_id);
    ThrowOnJavaException();
    return ret;
}

jobject JNIEnvWrapper::CallObjectMethod(jobject callee, jclass callee_class,
    const char * method_name, const char * method_signature,
    jobject param_1)
{
    jmethodID method_id = m_env->GetMethodID(callee_class,
        method_name, method_signature);
    ThrowOnJavaException();
    jobject ret = m_env->CallObjectMethod(callee, method_id, param_1);
    ThrowOnJavaException();
    return ret;
}

jobject JNIEnvWrapper::CallStaticObjectMethod(jclass callee_class,
    const char * method_name, const char * method_signature)
{
    jmethodID method_id = m_env->GetStaticMethodID(callee_class,
        method_name, method_signature);
    ThrowOnJavaException();
    jobject ret = m_env->CallStaticObjectMethod(callee_class, method_id);
    ThrowOnJavaException();

    return ret;
}

jobject JNIEnvWrapper::CallStaticObjectMethod(jclass callee_class,
    const char * method_name, const char * method_signature, jobject param_1)
{
    jmethodID method_id = m_env->GetStaticMethodID(callee_class,
        method_name, method_signature);
    ThrowOnJavaException();
    jobject ret = m_env->CallStaticObjectMethod(callee_class,
        method_id, param_1);
    ThrowOnJavaException();

    return ret;
}

jobject JNIEnvWrapper::CallStaticObjectMethod(jclass callee_class,
    const char * method_name, const char * method_signature, jobject param_1,
    jobject param_2, jobject param_3)
{
    jmethodID method_id = m_env->GetStaticMethodID(callee_class,
        method_name, method_signature);
    ThrowOnJavaException();
    jobject ret = m_env->CallStaticObjectMethod(callee_class, method_id,
        param_1, param_2, param_3);
    ThrowOnJavaException();

    return ret;
}

jint JNIEnvWrapper::CallIntMethod(jobject callee, jclass callee_class,
    const char * method_name, const char * method_signature, jobject param_1)
{
    jmethodID method_id = m_env->GetMethodID(callee_class,
        method_name, method_signature);
    ThrowOnJavaException();
    jint ret = m_env->CallIntMethod(callee, method_id, param_1);
    ThrowOnJavaException();
    return ret;
}

void JNIEnvWrapper::CallVoidMethod(jobject callee, jclass callee_class,
    const char * method_name, const char * method_signature)
{
    jmethodID method_id = m_env->GetMethodID(callee_class,
        method_name, method_signature);
    ThrowOnJavaException();
    m_env->CallVoidMethod(callee, method_id);
    ThrowOnJavaException();
}

jstring JNIEnvWrapper::NewStringUTF(const char * utf8_string)
{
    jstring ret = m_env->NewStringUTF(utf8_string);
    ThrowOnJavaException();

    return ret;
}

const char * JNIEnvWrapper::GetStringUTFChars(jstring string)
{
    return m_env->GetStringUTFChars(string, NULL);
}

void JNIEnvWrapper::ReleaseStringUTFChars(jstring string, const char * utf8_chars)
{
    m_env->ReleaseStringUTFChars(string, utf8_chars);
}

jbyteArray JNIEnvWrapper::NewByteArray(size_t size)
{
    jbyteArray ret = m_env->NewByteArray(size);
    ThrowOnJavaException();

    return ret;
}

jbyte * JNIEnvWrapper::GetByteArrayElements(jbyteArray java_array)
{
    return m_env->GetByteArrayElements(java_array, NULL);
}

void JNIEnvWrapper::ReleaseByteArrayElements(jbyteArray java_array,
    jbyte * native_array)
{
    m_env->ReleaseByteArrayElements(java_array, native_array, 0);
}

static std::string GetTemporaryFilePath(JNIEnvWrapper& jni)
{
    // context = android.app.ActivityThread.currentActivityThread().getApplication()
    jclass thread_class = jni.FindClass("android/app/ActivityThread");
    jobject current_thread = jni.CallStaticObjectMethod(thread_class,
        "currentActivityThread", "()Landroid/app/ActivityThread;");
    jobject context = jni.CallObjectMethod(current_thread, thread_class,
        "getApplication", "()Landroid/app/Application;");

    // cache_dir = context.getCacheDir()
    jclass context_class = jni.FindClass("android/content/Context");
    jobject cache_dir = jni.CallObjectMethod(context, context_class,
        "getCacheDir", "()Ljava/io/File;");

    // temp_file = java.io.File.createTempFile("chuck_temp", "", cache_dir)
    jclass file_class = jni.FindClass("java/io/File");
    jobject temp_file = jni.CallStaticObjectMethod(file_class, "createTempFile",
        "(Ljava/lang/String;Ljava/lang/String;Ljava/io/File;)Ljava/io/File;",
        jni.NewStringUTF("chuck_temp"), jni.NewStringUTF(""), cache_dir);

    // temp_file_path = temp_file.getPath()
    jstring temp_file_path = (jstring)jni.CallObjectMethod(temp_file,
        file_class, "getPath", "()Ljava/lang/String;");

    const char * path_chars = jni.GetStringUTFChars(temp_file_path);
    std::string ret = path_chars;
    jni.ReleaseStringUTFChars(temp_file_path, path_chars);

    return ret;
}

FILE * ChuckAndroid::getTemporaryFile() noexcept
{
    JavaVM * jvm = getJVM();
    if( !jvm )
    {
        return NULL;
    }
    JNIEnvWrapper jni(jvm);
    if( !jni.IsValid() )
    {
        return NULL;
    }

    try
    {
        const std::string temp_path = GetTemporaryFilePath(jni);
        return fopen(temp_path.c_str(), "wb+");
    }
    catch (JNIEnvWrapperException)
    {
        return NULL;
    }
}

bool ChuckAndroid::copyJARURLFileToTemporary(const char * jar_url, int * fd)
    noexcept
{
    JavaVM * jvm = getJVM();
    if( !jvm )
    {
        return false;
    }
    JNIEnvWrapper jni(jvm);
    if( !jni.IsValid() )
    {
        return false;
    }

    int ret = 0;
    try
    {
        const std::string temp_path = GetTemporaryFilePath(jni);
        ret = open(temp_path.c_str(), O_RDWR | O_CREAT, 0600);
        if( ret == -1 )
        {
            return false;
        }
        // do not pollute temp directory
        if( unlink(temp_path.c_str()) == -1 )
        {
            return false;
        }

        // url = new java.net.URL(jar_url)
        jclass url_class = jni.FindClass("java/net/URL");
        jobject url = jni.ConstructNewObject(url_class,
            "(Ljava/lang/String;)V", jni.NewStringUTF(jar_url));

        // connection = url.openConnection()
        jobject connection = jni.CallObjectMethod(url, url_class,
            "openConnection", "()Ljava/net/URLConnection;");

        // jar_file = (java.net.JarURLConnection)connection.getJarFile()
        jclass jar_url_connection_class = jni.FindClass(
            "java/net/JarURLConnection");
        jobject jar_file = jni.CallObjectMethod(connection,
            jar_url_connection_class, "getJarFile",
            "()Ljava/util/jar/JarFile;");

        // jar_entry = connection.getJarEntry()
        jobject jar_entry = jni.CallObjectMethod(connection,
            jar_url_connection_class, "getJarEntry",
            "()Ljava/util/jar/JarEntry;");

        // input_stream = jar_file.getInputStream(jar_entry)
        jclass jar_file_class = jni.FindClass("java/util/jar/JarFile");
        jobject input_stream = jni.CallObjectMethod(jar_file, jar_file_class,
            "getInputStream", "(Ljava/util/zip/ZipEntry;)Ljava/io/InputStream;",
            jar_entry);

        // buffer = new byte[8 * 1024]
        jbyteArray java_buffer = jni.NewByteArray(8 * 1024);
        // s = input_stream.read(buffer)
        jclass input_stream_class = jni.FindClass("java/io/InputStream");
        jint s = 0;
        while( true )
        {
            s = jni.CallIntMethod(input_stream, input_stream_class, "read",
                "([B)I", java_buffer);
            if( s < 0 )
            {
                break;
            }
            jbyte * native_buffer = jni.GetByteArrayElements(java_buffer);
            size_t written = 0;
            while( written < s )
            {
                ssize_t written_now = write(ret, native_buffer + written,
                    s - written);
                if( written_now < 0 )
                {
                    close(ret);
                    return false;
                }
                written += written_now;
            }
            jni.ReleaseByteArrayElements(java_buffer, native_buffer);
        }

        // jar_file.close()
        jni.CallVoidMethod(jar_file, jar_file_class, "close", "()V");
    }
    catch (JNIEnvWrapperException)
    {
        if( ret != -1 )
        {
            close(ret);
        }
        return false;
    }

    lseek(ret, 0, SEEK_SET);
    *fd = ret;
    return true;
}

#endif // __ANDROID__
