/*------------------------------------------------------------------------
* (The MIT License)
* 
* Copyright (c) 2008-2011 Rhomobile, Inc.
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
* 
* http://rhomobile.com
*------------------------------------------------------------------------*/

#include "rhodes/JNIRhodes.h"
#include "rhodes/JNIRhoRuby.h"

#undef DEFAULT_LOGCATEGORY
#define DEFAULT_LOGCATEGORY "JNIRhoRuby"

jclass RhoValueConverter::clsHashMap;
jclass RhoValueConverter::clsVector;

jmethodID RhoValueConverter::midHashMapConstructor;
jmethodID RhoValueConverter::midVectorConstructor;
jmethodID RhoValueConverter::midPut;
jmethodID RhoValueConverter::midAddElement;

bool RhoValueConverter::init = false;


RhoValueConverter::RhoValueConverter(JNIEnv *e) : env(e)
{
    if(!init)
    {
        clsHashMap = getJNIClass(RHODES_JAVA_CLASS_HASHMAP);
        if (!clsHashMap) return;
        clsVector = getJNIClass(RHODES_JAVA_CLASS_VECTOR);
        if (!clsVector) return;
        midHashMapConstructor = getJNIClassMethod(env, clsHashMap, "<init>", "()V");
        if (!midHashMapConstructor) return;
        midVectorConstructor = getJNIClassMethod(env, clsVector, "<init>", "()V");
        if (!midVectorConstructor) return;
        midPut = getJNIClassMethod(env, clsHashMap, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
        if (!midPut) return;
        midAddElement = getJNIClassMethod(env, clsVector, "addElement", "(Ljava/lang/Object;)V");
        if (!midAddElement) return;
        init = true;
    }
}

jobject RhoValueConverter::createObject(rho_param *p)
{
    if (!init || !p)
        return 0;

    switch (p->type) {
    case RHO_PARAM_STRING:
        return rho_cast<jstring>(env, p->v.string);
        break;
    case RHO_PARAM_ARRAY:
        {
            jobject v = env->NewObject(clsVector, midVectorConstructor);
            if (!v) return NULL;

            for (int i = 0, lim = p->v.array->size; i < lim; ++i) {
                jhobject value = createObject(p->v.array->value[i]);
                env->CallVoidMethod(v, midAddElement, value.get());
            }
            return v;
        }
        break;
    case RHO_PARAM_HASH:
        {
            jobject v = env->NewObject(clsHashMap, midHashMapConstructor);
            if (!v) return NULL;

            for (int i = 0, lim = p->v.hash->size; i < lim; ++i) {
                jhstring key = rho_cast<jstring>(p->v.hash->name[i]);
                jhobject value = createObject(p->v.hash->value[i]);
                env->CallObjectMethod(v, midPut, key.get(), value.get());
            }
            return v;
        }
        break;
    default:
        return 0;
    }
}

extern "C"
{
    static void rubyHashElementToProperty(const char* key, VALUE valElement, void* data)
    {
        PropertyMapConvertor<VALUE>* pThis = reinterpret_cast<PropertyMapConvertor<VALUE>* >(data);
        jhstring jhKey = rho_cast<jstring>(pThis->m_env, key);
        jhobject jhVal = rho_cast<jstring>(pThis->m_env, rb_funcall(valElement, rb_intern("to_s"), 0));

        jhobject jhPrev = pThis->m_env->CallObjectMethod(pThis->m_jObject, details::RhoJniConvertor::midHashMapPut, jhKey.get(), jhVal.get());
    }
}

jobject PropertyMapConvertor<VALUE>::convertToPropertyMap(JNIEnv *env, VALUE value)
{
    if (NIL_P(value))
        return 0;

    if (!initConvertor(env))
        return 0;

    m_jObject = env->NewObject(clsHashMap, midHashMap);
    if(env->ExceptionCheck() == JNI_TRUE)
    {
        rho::String message = rho::common::clearException(env);
        RAWLOG_ERROR(message.c_str());
        return 0;
    }

    rho_ruby_enum_hash(value, rubyHashElementToProperty, this);

    return m_jObject;
}

namespace details {

VALUE rho_cast_helper<VALUE, jobject>::convertJavaMapToRubyHash(jobject objMap)
{
    jhobject objSet = m_env->CallObjectMethod(objMap, midMapKeySet);
    if(m_env->ExceptionCheck() == JNI_TRUE)
    {
        rho::String message = rho::common::clearException(m_env);
        RAWLOG_ERROR(message.c_str());
        return Qnil;
    }
    jhobject objIterator = m_env->CallObjectMethod(objSet.get(), midSetIterator);
    if(m_env->ExceptionCheck() == JNI_TRUE)
    {
        rho::String message = rho::common::clearException(m_env);
        RAWLOG_ERROR(message.c_str());
        return Qnil;
    }
                                  
    CHoldRubyValue retval(rho_ruby_createHash());
    while(m_env->CallBooleanMethod(objIterator.get(), midIteratorHasNext))
    {
        jhobject jhKey = m_env->CallObjectMethod(objIterator.get(), midIteratorNext);
        if (!jhKey) return Qnil;
        jhobject jhVal = m_env->CallObjectMethod(objMap, midMapGet, jhKey.get());
        if (!jhVal) return Qnil;

        CHoldRubyValue key(rho_cast<VALUE>(m_env, jhKey));
        CHoldRubyValue val(rho_cast<VALUE>(m_env, jhVal));

        rho_ruby_add_to_hash(retval, key, val);
    }
    return retval;
}

VALUE rho_cast_helper<VALUE, jobject>::convertJavaCollectionToRubyArray(jobject jList)
{
    jhobject jhIterator = m_env->CallObjectMethod(jList, midCollectionIterator);
    if(m_env->ExceptionCheck() == JNI_TRUE)
    {
        rho::String message = rho::common::clearException(m_env);
        RAWLOG_ERROR(message.c_str());
        return Qnil;
    }

    CHoldRubyValue retval(rho_ruby_create_array());
    while(m_env->CallBooleanMethod(jhIterator.get(), midIteratorHasNext))
    {
        jhobject jhVal = m_env->CallObjectMethod(jhIterator.get(), midIteratorNext);
        if(m_env->ExceptionCheck() == JNI_TRUE)
        {
            rho::String message = rho::common::clearException(m_env);
            RAWLOG_ERROR(message.c_str());
            return Qnil;
        }

        CHoldRubyValue val(rho_cast<VALUE>(m_env, jhVal));
        rho_ruby_add_to_array(retval, val);
    }
    return retval;
}

VALUE rho_cast_helper<VALUE, jobject>::getBoolean(jobject jBoolean)
{
    jboolean jRes = m_env->CallBooleanMethod(jBoolean, midBooleanValue);
    return static_cast<bool>(jRes) ? Qtrue : Qfalse;
}

VALUE rho_cast_helper<VALUE, jobject>::getInteger(jobject jInteger)
{
    jint jRes = m_env->CallIntMethod(jInteger, midIntValue);
    return rho_ruby_create_integer(jRes);
}

VALUE rho_cast_helper<VALUE, jobject>::getDouble(jobject jDouble)
{
    jboolean jRes = m_env->CallDoubleMethod(jDouble, midDoubleValue);
    return rho_ruby_create_double(jRes);
}

extern "C"
{

static void ruby_array_each(VALUE val, void* param)
{
    rho_cast_helper<jobject, VALUE>* pThis = reinterpret_cast<rho_cast_helper<jobject, VALUE>* >(param);
    jhobject jhVal = rho_cast<jobject>(pThis->m_env, val);

    pThis->m_env->CallBooleanMethod(pThis->m_jObject, RhoJniConvertor::midArrayListAdd, jhVal.get());
}

static void ruby_hash_each(const char* key, VALUE val, void* param)
{
    rho_cast_helper<jobject, VALUE>* pThis = reinterpret_cast<rho_cast_helper<jobject, VALUE>* >(param);
    jhstring jhKey = rho_cast<jstring>(pThis->m_env, key);
    jhobject jhVal = rho_cast<jobject>(pThis->m_env, val);

    jhobject jhPrev = pThis->m_env->CallObjectMethod(pThis->m_jObject, RhoJniConvertor::midHashMapPut, jhKey.get(), jhVal.get());
}
}

jobject rho_cast_helper<jobject, VALUE>::convertRubyArrayToJavaCollection(VALUE array)
{
    m_jObject = m_env->NewObject(clsArrayList, midArrayList);
    if(m_env->ExceptionCheck() == JNI_TRUE)
    {
        rho::String message = rho::common::clearException(m_env);
        RAWLOG_ERROR(message.c_str());
        return 0;
    }

    rho_ruby_enum_ary(array, ruby_array_each, this);

    return m_jObject;
}

jobject rho_cast_helper<jobject, VALUE>::convertRubyHashToJavaMap(VALUE hash)
{
    m_jObject = m_env->NewObject(clsHashMap, midHashMap);
    if(m_env->ExceptionCheck() == JNI_TRUE)
    {
        rho::String message = rho::common::clearException(m_env);
        RAWLOG_ERROR(message.c_str());
        return 0;
    }

    rho_ruby_enum_hash(hash, ruby_hash_each, this);

    return m_jObject;
}

jobject rho_cast_helper<jobject, VALUE>::operator()(JNIEnv *env, VALUE value)
{
    RAWTRACE("rho_cast<jobject, VALUE>");

    if (NIL_P(value))
        return 0;

    if (!initConvertor(env))
        return 0;

    switch(TYPE(value))
    {
    case RUBY_T_SYMBOL:
        value = rb_funcall(value, rb_intern("to_s"), 0);
    case RUBY_T_STRING:
        RAWTRACE("Convert to String object");
        return env->NewStringUTF(RSTRING_PTR(value));
    case RUBY_T_ARRAY:
        RAWTRACE("Convert to Collection object");
        return convertRubyArrayToJavaCollection(value);
    case RUBY_T_HASH:
        RAWTRACE("Convert to Map object");
        return convertRubyHashToJavaMap(value);
    case RUBY_T_TRUE:
        RAWTRACE("Convert to TRUE Boolean obeject");
        return RhoJniConvertor::getBooleanObject(true);
    case RUBY_T_FALSE:
        RAWTRACE("Convert to FALSE Boolean object");
        return RhoJniConvertor::getBooleanObject(false);
    case RUBY_T_FIXNUM:
    case RUBY_T_BIGNUM:
        RAWTRACE("Convert to Integer object");
        return RhoJniConvertor::getIntegerObject(NUM2LONG(value));
    case RUBY_T_FLOAT:
    case RUBY_T_RATIONAL:
        RAWTRACE("Convert to Double object");
        return RhoJniConvertor::getDoubleObject(NUM2DBL(value));
    }

    RAWLOG_ERROR1("rho_cast<jobject, VALUE>: wrong type of VALUE: %d", TYPE(value));
    return 0;
}

VALUE rho_cast_helper<VALUE, jobject>::operator()(JNIEnv *env, jobject obj)
{
    RAWTRACE("rho_cast<VALUE, jobject>");

    if(env->IsSameObject(obj, NULL) == JNI_TRUE)
        return Qnil;

    if(!initConvertor(env))
    {
        env->ThrowNew(getJNIClass(RHODES_JAVA_CLASS_RUNTIME_EXCEPTION), "Java <=> Ruby conversion initialization failed");
        return Qnil;
    }

    if(env->IsInstanceOf(obj, clsString))
    {
        const char *str = env->GetStringUTFChars(static_cast<jstring>(obj), JNI_FALSE);
        VALUE res = rho_ruby_create_string(str);
        env->ReleaseStringUTFChars(static_cast<jstring>(obj), str);
        return res;
    }
    else
    if(env->IsInstanceOf(obj, clsBoolean))
        return getBoolean(obj);
    else
    if(env->IsInstanceOf(obj, clsInteger))
        return getInteger(obj);
    else
    if(env->IsInstanceOf(obj, clsDouble))
        return getDouble(obj);
    else
    if(env->IsInstanceOf(obj, clsCollection))
        return convertJavaCollectionToRubyArray(obj);
    else
    if(env->IsInstanceOf(obj, clsMap))
        return convertJavaMapToRubyHash(obj);

    RAWLOG_ERROR("rho_cast<VALUE, jobject>: unknown type of value");
    return Qnil;
}

VALUE rho_cast_helper<VALUE, jstring>::operator()(JNIEnv *env, jstring jStr)
{
    RAWTRACE("rho_cast<VALUE, jstring>");

    if(env->IsSameObject(jStr, NULL) == JNI_TRUE)
        return Qnil;

    const char *str = env->GetStringUTFChars(jStr, JNI_FALSE);
    VALUE res = rho_ruby_create_string(str);
    env->ReleaseStringUTFChars(jStr, str);

    return res;
}

jstring rho_cast_helper<jstring, VALUE>::operator()(JNIEnv *env, VALUE value)
{
    RAWTRACE("rho_cast<jstring, VALUE>");

    if (NIL_P(value))
        return 0;

    return env->NewStringUTF(RSTRING_PTR(value));
}

jobjectArray rho_cast_helper<jobjectArray, VALUE>::operator()(JNIEnv *env, VALUE value)
{
    RAWTRACE("rho_cast<jobjectArray, VALUE>");

    if (!initConvertor(env))
        return 0;

    if (NIL_P(value))
        return 0;

    if(TYPE(value) == RUBY_T_ARRAY)
    {
        int size = RARRAY_LEN(value);
        jobjectArray jArray = env->NewObjectArray(size, clsString, 0);

        if(env->ExceptionCheck() == JNI_TRUE)
        {
            rho::String message = rho::common::clearException(env);
            RAWLOG_ERROR(message.c_str());
            return 0;
        }

        for (int i = 0; i < size; ++i)
        {
            jhstring jhElement = rho_cast<jstring>(env, rb_ary_entry(value, i));
            env->SetObjectArrayElement(jArray, i, jhElement.get());
        }
        return jArray;
    }

    RAWLOG_ERROR1("rho_cast<jobjectArray, VALUE>: wrong type of VALUE: %d", TYPE(value));
    return 0;
}

jboolean rho_cast_helper<jboolean, VALUE>::operator()(JNIEnv *env, VALUE value)
{
    RAWTRACE("rho_cast<jboolean, VALUE>");

    if (NIL_P(value))
        return 0;

    if(TYPE(value) == RUBY_T_TRUE)
    {
        return static_cast<jboolean>(true);
    }
    if(TYPE(value) == RUBY_T_FALSE)
    {
        return static_cast<jboolean>(false);
    }
    if(TYPE(value) == RUBY_T_FIXNUM)
    {
        return static_cast<jboolean>(rho_ruby_get_int(value) != 0);
    }
    RAWLOG_ERROR1("rho_cast<jboolean, VALUE>: wrong type of VALUE: %d", TYPE(value));
    return 0;
}

jint rho_cast_helper<jint, VALUE>::operator()(JNIEnv *env, VALUE value)
{
    RAWTRACE("rho_cast<jint, VALUE>");

    if (NIL_P(value))
        return 0;

    if(TYPE(value) == RUBY_T_FIXNUM || TYPE(value) == RUBY_T_BIGNUM)
    {
        return static_cast<jint>(NUM2LONG(value));
    }

    RAWLOG_ERROR1("rho_cast<jint, VALUE>: wrong type of VALUE: %d", TYPE(value));
    return 0;
}

jdouble rho_cast_helper<jdouble, VALUE>::operator()(JNIEnv *env, VALUE value)
{
    RAWTRACE("rho_cast<jdouble, VALUE>");

    if (NIL_P(value))
        return 0;

    if(TYPE(value) == RUBY_T_FLOAT ||
       TYPE(value) == RUBY_T_FIXNUM ||
       TYPE(value) == RUBY_T_BIGNUM ||
       TYPE(value) == RUBY_T_RATIONAL)
    {
        return static_cast<jdouble>(NUM2DBL(value));
    }

    RAWLOG_ERROR1("rho_cast<jdouble, VALUE>: wrong type of VALUE: %d", TYPE(value));
    return 0;
}

VALUE rho_cast_helper<VALUE, jboolean>::operator ()(JNIEnv *env, jboolean jValue)
{
    return rho_ruby_create_boolean(static_cast<bool>(jValue));
}

VALUE rho_cast_helper<VALUE, jint>::operator ()(JNIEnv *env, jint jValue)
{
    return rho_ruby_create_integer(static_cast<int>(jValue));
}

VALUE rho_cast_helper<VALUE, jdouble>::operator ()(JNIEnv *env, jdouble jValue)
{
    return rho_ruby_create_double(static_cast<double>(jValue));
}

}