/*
 * Copyright (C) 2009-2010 Google Inc.
 *
 * 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.
 */

#include <stdio.h>
#include <unistd.h>

#define LOG_TAG "SynthProxyJNI"

#include <utils/Log.h>
#include <nativehelper/jni.h>
#include <nativehelper/JNIHelp.h>
#include <android_runtime/AndroidRuntime.h>
#include <android_runtime/Log.h>
#include <math.h>

#include <dlfcn.h>

#include "tts.h"

#define DEFAULT_TTS_RATE        16000
#define DEFAULT_TTS_BUFFERSIZE  2048

// EQ + BOOST parameters
#define FILTER_LOWSHELF_ATTENUATION -18.0f // in dB
#define FILTER_TRANSITION_FREQ 1100.0f     // in Hz
#define FILTER_SHELF_SLOPE 1.0f            // Q
#define FILTER_GAIN 5.5f // linear gain

// android.media.AudioFormat.ENCODING_ values
//
// Note that these constants are different from those
// defined in the native code (system/audio.h and others).
// We use them because we use a Java AudioTrack to play
// back our data.
#define AUDIO_FORMAT_ENCODING_DEFAULT 1
#define AUDIO_FORMAT_ENCODING_PCM_16_BIT 2
#define AUDIO_FORMAT_ENCODING_PCM_8_BIT 3

using namespace android;

// ----------------------------------------------------------------------------
// EQ data
static double m_fa, m_fb, m_fc, m_fd, m_fe;
static double x0;  // x[n]
static double x1;  // x[n-1]
static double x2;  // x[n-2]
static double out0;// y[n]
static double out1;// y[n-1]
static double out2;// y[n-2]

static float fFilterLowshelfAttenuation = FILTER_LOWSHELF_ATTENUATION;
static float fFilterTransitionFreq = FILTER_TRANSITION_FREQ;
static float fFilterShelfSlope = FILTER_SHELF_SLOPE;
static float fFilterGain = FILTER_GAIN;
static bool  bUseFilter = false;

void initializeEQ() {
    double amp = float(pow(10.0, fFilterLowshelfAttenuation / 40.0));
    double w = 2.0 * M_PI * (fFilterTransitionFreq / DEFAULT_TTS_RATE);
    double sinw = float(sin(w));
    double cosw = float(cos(w));
    double beta = float(sqrt(amp)/fFilterShelfSlope);

    // initialize low-shelf parameters
    double b0 = amp * ((amp+1.0F) - ((amp-1.0F)*cosw) + (beta*sinw));
    double b1 = 2.0F * amp * ((amp-1.0F) - ((amp+1.0F)*cosw));
    double b2 = amp * ((amp+1.0F) - ((amp-1.0F)*cosw) - (beta*sinw));
    double a0 = (amp+1.0F) + ((amp-1.0F)*cosw) + (beta*sinw);
    double a1 = 2.0F * ((amp-1.0F) + ((amp+1.0F)*cosw));
    double a2 = -((amp+1.0F) + ((amp-1.0F)*cosw) - (beta*sinw));

    m_fa = fFilterGain * b0/a0;
    m_fb = fFilterGain * b1/a0;
    m_fc = fFilterGain * b2/a0;
    m_fd = a1/a0;
    m_fe = a2/a0;
}

void initializeFilter() {
    x0 = 0.0f;
    x1 = 0.0f;
    x2 = 0.0f;
    out0 = 0.0f;
    out1 = 0.0f;
    out2 = 0.0f;
}

void applyFilter(int16_t* buffer, size_t sampleCount) {

    for (size_t i=0 ; i<sampleCount ; i++) {

        x0 = (double) buffer[i];

        out0 = (m_fa*x0) + (m_fb*x1) + (m_fc*x2) + (m_fd*out1) + (m_fe*out2);

        x2 = x1;
        x1 = x0;

        out2 = out1;
        out1 = out0;

        if (out0 > 32767.0f) {
            buffer[i] = 32767;
        } else if (out0 < -32768.0f) {
            buffer[i] = -32768;
        } else {
            buffer[i] = (int16_t) out0;
        }
    }
}


// ----------------------------------------------------------------------------

static jmethodID synthesisRequest_start;
static jmethodID synthesisRequest_audioAvailable;
static jmethodID synthesisRequest_done;

static Mutex engineMutex;



typedef android_tts_engine_t *(*android_tts_entrypoint)();

// ----------------------------------------------------------------------------
class SynthProxyJniStorage {
  public:
    android_tts_engine_t *mEngine;
    void *mEngineLibHandle;
    int8_t *mBuffer;
    size_t mBufferSize;

    SynthProxyJniStorage() {
        mEngine = NULL;
        mEngineLibHandle = NULL;
        mBufferSize = DEFAULT_TTS_BUFFERSIZE;
        mBuffer = new int8_t[mBufferSize];
        memset(mBuffer, 0, mBufferSize);
    }

    ~SynthProxyJniStorage() {
        if (mEngine) {
            mEngine->funcs->shutdown(mEngine);
            mEngine = NULL;
        }
        if (mEngineLibHandle) {
            int res = dlclose(mEngineLibHandle);
            ALOGE_IF( res != 0, "~SynthProxyJniStorage(): dlclose returned %d", res);
        }
        delete[] mBuffer;
    }

};

// ----------------------------------------------------------------------------

struct SynthRequestData {
    SynthProxyJniStorage *jniStorage;
    JNIEnv *env;
    jobject request;
    bool startCalled;
};

// ----------------------------------------------------------------------------

/*
 * Calls into Java
 */

static bool checkException(JNIEnv *env)
{
    jthrowable ex = env->ExceptionOccurred();
    if (ex == NULL) {
        return false;
    }
    env->ExceptionClear();
    LOGE_EX(env, ex);
    env->DeleteLocalRef(ex);
    return true;
}

static int callRequestStart(JNIEnv *env, jobject request,
        uint32_t rate, android_tts_audio_format_t format, int channelCount)
{
    int encoding;

    switch (format) {
    case ANDROID_TTS_AUDIO_FORMAT_DEFAULT:
        encoding = AUDIO_FORMAT_ENCODING_DEFAULT;
        break;
    case ANDROID_TTS_AUDIO_FORMAT_PCM_8_BIT:
        encoding = AUDIO_FORMAT_ENCODING_PCM_8_BIT;
        break;
    case ANDROID_TTS_AUDIO_FORMAT_PCM_16_BIT:
        encoding = AUDIO_FORMAT_ENCODING_PCM_16_BIT;
        break;
    default:
        ALOGE("Can't play, bad format");
        return ANDROID_TTS_FAILURE;
    }

    int result = env->CallIntMethod(request, synthesisRequest_start, rate, encoding, channelCount);
    if (checkException(env)) {
        return ANDROID_TTS_FAILURE;
    }
    return result;
}

static int callRequestAudioAvailable(JNIEnv *env, jobject request, int8_t *buffer,
        int offset, int length)
{
    // TODO: Not nice to have to copy the buffer. Use ByteBuffer?
    jbyteArray javaBuffer = env->NewByteArray(length);
    if (javaBuffer == NULL) {
        ALOGE("Failed to allocate byte array");
        return ANDROID_TTS_FAILURE;
    }

    env->SetByteArrayRegion(javaBuffer, 0, length, static_cast<jbyte *>(buffer + offset));
    if (checkException(env)) {
        env->DeleteLocalRef(javaBuffer);
        return ANDROID_TTS_FAILURE;
    }
    int result = env->CallIntMethod(request, synthesisRequest_audioAvailable,
            javaBuffer, offset, length);
    if (checkException(env)) {
        env->DeleteLocalRef(javaBuffer);
        return ANDROID_TTS_FAILURE;
    }
    env->DeleteLocalRef(javaBuffer);
    return result;
}

static int callRequestDone(JNIEnv *env, jobject request)
{
    int result = env->CallIntMethod(request, synthesisRequest_done);
    if (checkException(env)) {
        return ANDROID_TTS_FAILURE;
    }
    return result;
}

/*
 * Callback from TTS engine.
 */
extern "C" android_tts_callback_status_t
__ttsSynthDoneCB(void **pUserdata, uint32_t rate,
               android_tts_audio_format_t format, int channelCount,
               int8_t **pWav, size_t *pBufferSize,
               android_tts_synth_status_t status)
{
    if (*pUserdata == NULL){
        ALOGE("userdata == NULL");
        return ANDROID_TTS_CALLBACK_HALT;
    }

    SynthRequestData *pRequestData = static_cast<SynthRequestData*>(*pUserdata);
    SynthProxyJniStorage *pJniData = pRequestData->jniStorage;
    JNIEnv *env = pRequestData->env;

    if (*pWav != NULL && *pBufferSize > 0) {
        if (bUseFilter) {
            applyFilter(reinterpret_cast<int16_t*>(*pWav), *pBufferSize/2);
        }

        if (!pRequestData->startCalled) {
            // TODO: is encoding one of the AudioFormat.ENCODING_* constants?
            pRequestData->startCalled = true;
            if (callRequestStart(env, pRequestData->request, rate, format, channelCount)
                    != ANDROID_TTS_SUCCESS) {
                return ANDROID_TTS_CALLBACK_HALT;
            }
        }

        if (callRequestAudioAvailable(env, pRequestData->request, *pWav, 0, *pBufferSize)
                != ANDROID_TTS_SUCCESS) {
            return ANDROID_TTS_CALLBACK_HALT;
        }

        memset(*pWav, 0, *pBufferSize);
    }

    if (pWav == NULL || status == ANDROID_TTS_SYNTH_DONE) {
        callRequestDone(env, pRequestData->request);
        env->DeleteGlobalRef(pRequestData->request);
        delete pRequestData;
        pRequestData = NULL;
        return ANDROID_TTS_CALLBACK_HALT;
    }

    *pBufferSize = pJniData->mBufferSize;

    return ANDROID_TTS_CALLBACK_CONTINUE;
}


// ----------------------------------------------------------------------------
static jint
com_android_tts_compat_SynthProxy_setLowShelf(JNIEnv *env, jobject thiz, jboolean applyFilter,
        jfloat filterGain, jfloat attenuationInDb, jfloat freqInHz, jfloat slope)
{
    bUseFilter = applyFilter;
    if (applyFilter) {
        fFilterLowshelfAttenuation = attenuationInDb;
        fFilterTransitionFreq = freqInHz;
        fFilterShelfSlope = slope;
        fFilterGain = filterGain;

        if (fFilterShelfSlope != 0.0f) {
            initializeEQ();
        } else {
            ALOGE("Invalid slope, can't be null");
            return ANDROID_TTS_FAILURE;
        }
    }

    return ANDROID_TTS_SUCCESS;
}

// ----------------------------------------------------------------------------
static jlong
com_android_tts_compat_SynthProxy_native_setup(JNIEnv *env, jobject thiz,
        jstring nativeSoLib, jstring engConfig)
{
    jlong result = 0;
    bUseFilter = false;

    const char *nativeSoLibNativeString =  env->GetStringUTFChars(nativeSoLib, 0);
    const char *engConfigString = env->GetStringUTFChars(engConfig, 0);

    void *engine_lib_handle = dlopen(nativeSoLibNativeString,
            RTLD_NOW | RTLD_LOCAL);
    if (engine_lib_handle == NULL) {
        ALOGE("com_android_tts_compat_SynthProxy_native_setup(): engine_lib_handle == NULL");
    } else {
        android_tts_entrypoint get_TtsEngine =
            reinterpret_cast<android_tts_entrypoint>(dlsym(engine_lib_handle, "android_getTtsEngine"));

        // Support obsolete/legacy binary modules
        if (get_TtsEngine == NULL) {
            get_TtsEngine =
                reinterpret_cast<android_tts_entrypoint>(dlsym(engine_lib_handle, "getTtsEngine"));
        }

        android_tts_engine_t *engine = (*get_TtsEngine)();
        if (engine) {
            Mutex::Autolock l(engineMutex);
            engine->funcs->init(engine, __ttsSynthDoneCB, engConfigString);

            SynthProxyJniStorage *pSynthData = new SynthProxyJniStorage();
            pSynthData->mEngine = engine;
            pSynthData->mEngineLibHandle = engine_lib_handle;
            result = reinterpret_cast<jlong>(pSynthData);
        }
    }

    env->ReleaseStringUTFChars(nativeSoLib, nativeSoLibNativeString);
    env->ReleaseStringUTFChars(engConfig, engConfigString);

    return result;
}

static SynthProxyJniStorage *getSynthData(jlong jniData)
{
    if (jniData == 0) {
        ALOGE("Engine not initialized");
        return NULL;
    }
    return reinterpret_cast<SynthProxyJniStorage *>(jniData);
}

static void
com_android_tts_compat_SynthProxy_native_finalize(JNIEnv *env, jobject thiz, jlong jniData)
{
    SynthProxyJniStorage* pSynthData = getSynthData(jniData);
    if (pSynthData == NULL) {
        return;
    }

    Mutex::Autolock l(engineMutex);

    delete pSynthData;
}

static void
com_android_tts_compat_SynthProxy_shutdown(JNIEnv *env, jobject thiz, jlong jniData)
{
    com_android_tts_compat_SynthProxy_native_finalize(env, thiz, jniData);
}

static jint
com_android_tts_compat_SynthProxy_isLanguageAvailable(JNIEnv *env, jobject thiz, jlong jniData,
        jstring language, jstring country, jstring variant)
{
    SynthProxyJniStorage* pSynthData = getSynthData(jniData);
    if (pSynthData == NULL) {
        return ANDROID_TTS_LANG_NOT_SUPPORTED;
    }

    android_tts_engine_t *engine = pSynthData->mEngine;
    if (!engine) {
        return ANDROID_TTS_LANG_NOT_SUPPORTED;
    }

    const char *langNativeString = env->GetStringUTFChars(language, 0);
    const char *countryNativeString = env->GetStringUTFChars(country, 0);
    const char *variantNativeString = env->GetStringUTFChars(variant, 0);

    int result = engine->funcs->isLanguageAvailable(engine, langNativeString,
            countryNativeString, variantNativeString);

    env->ReleaseStringUTFChars(language, langNativeString);
    env->ReleaseStringUTFChars(country, countryNativeString);
    env->ReleaseStringUTFChars(variant, variantNativeString);

    return (jint) result;
}

static jint
com_android_tts_compat_SynthProxy_setLanguage(JNIEnv *env, jobject thiz, jlong jniData,
        jstring language, jstring country, jstring variant)
{
    SynthProxyJniStorage* pSynthData = getSynthData(jniData);
    if (pSynthData == NULL) {
        return ANDROID_TTS_LANG_NOT_SUPPORTED;
    }

    Mutex::Autolock l(engineMutex);

    android_tts_engine_t *engine = pSynthData->mEngine;
    if (!engine) {
        return ANDROID_TTS_LANG_NOT_SUPPORTED;
    }

    const char *langNativeString = env->GetStringUTFChars(language, 0);
    const char *countryNativeString = env->GetStringUTFChars(country, 0);
    const char *variantNativeString = env->GetStringUTFChars(variant, 0);

    int result = engine->funcs->setLanguage(engine, langNativeString,
            countryNativeString, variantNativeString);

    env->ReleaseStringUTFChars(language, langNativeString);
    env->ReleaseStringUTFChars(country, countryNativeString);
    env->ReleaseStringUTFChars(variant, variantNativeString);

    return (jint) result;
}


static jint
com_android_tts_compat_SynthProxy_loadLanguage(JNIEnv *env, jobject thiz, jlong jniData,
        jstring language, jstring country, jstring variant)
{
    SynthProxyJniStorage* pSynthData = getSynthData(jniData);
    if (pSynthData == NULL) {
        return ANDROID_TTS_LANG_NOT_SUPPORTED;
    }

    android_tts_engine_t *engine = pSynthData->mEngine;
    if (!engine) {
        return ANDROID_TTS_LANG_NOT_SUPPORTED;
    }

    const char *langNativeString = env->GetStringUTFChars(language, 0);
    const char *countryNativeString = env->GetStringUTFChars(country, 0);
    const char *variantNativeString = env->GetStringUTFChars(variant, 0);

    int result = engine->funcs->loadLanguage(engine, langNativeString,
            countryNativeString, variantNativeString);

    env->ReleaseStringUTFChars(language, langNativeString);
    env->ReleaseStringUTFChars(country, countryNativeString);
    env->ReleaseStringUTFChars(variant, variantNativeString);

    return (jint) result;
}

static jint
com_android_tts_compat_SynthProxy_setProperty(JNIEnv *env, jobject thiz, jlong jniData,
        jstring name, jstring value)
{
    SynthProxyJniStorage* pSynthData = getSynthData(jniData);
    if (pSynthData == NULL) {
        return ANDROID_TTS_FAILURE;
    }

    Mutex::Autolock l(engineMutex);

    android_tts_engine_t *engine = pSynthData->mEngine;
    if (!engine) {
        return ANDROID_TTS_FAILURE;
    }

    const char *nameChars = env->GetStringUTFChars(name, 0);
    const char *valueChars = env->GetStringUTFChars(value, 0);
    size_t valueLength = env->GetStringUTFLength(value);

    int result = engine->funcs->setProperty(engine, nameChars, valueChars, valueLength);

    env->ReleaseStringUTFChars(name, nameChars);
    env->ReleaseStringUTFChars(name, valueChars);

    return (jint) result;
}

static jint
com_android_tts_compat_SynthProxy_speak(JNIEnv *env, jobject thiz, jlong jniData,
        jstring textJavaString, jobject request)
{
    SynthProxyJniStorage* pSynthData = getSynthData(jniData);
    if (pSynthData == NULL) {
        return ANDROID_TTS_FAILURE;
    }

    initializeFilter();

    Mutex::Autolock l(engineMutex);

    android_tts_engine_t *engine = pSynthData->mEngine;
    if (!engine) {
        return ANDROID_TTS_FAILURE;
    }

    SynthRequestData *pRequestData = new SynthRequestData;
    pRequestData->jniStorage = pSynthData;
    pRequestData->env = env;
    pRequestData->request = env->NewGlobalRef(request);
    pRequestData->startCalled = false;

    const char *textNativeString = env->GetStringUTFChars(textJavaString, 0);
    memset(pSynthData->mBuffer, 0, pSynthData->mBufferSize);

    int result = engine->funcs->synthesizeText(engine, textNativeString,
            pSynthData->mBuffer, pSynthData->mBufferSize, static_cast<void *>(pRequestData));
    env->ReleaseStringUTFChars(textJavaString, textNativeString);

    return (jint) result;
}

static jint
com_android_tts_compat_SynthProxy_stop(JNIEnv *env, jobject thiz, jlong jniData)
{
    SynthProxyJniStorage* pSynthData = getSynthData(jniData);
    if (pSynthData == NULL) {
        return ANDROID_TTS_FAILURE;
    }

    android_tts_engine_t *engine = pSynthData->mEngine;
    if (!engine) {
        return ANDROID_TTS_FAILURE;
    }

    return (jint) engine->funcs->stop(engine);
}

static jint
com_android_tts_compat_SynthProxy_stopSync(JNIEnv *env, jobject thiz, jlong jniData)
{
    SynthProxyJniStorage* pSynthData = getSynthData(jniData);
    if (pSynthData == NULL) {
        return ANDROID_TTS_FAILURE;
    }

    // perform a regular stop
    int result = com_android_tts_compat_SynthProxy_stop(env, thiz, jniData);
    // but wait on the engine having released the engine mutex which protects
    // the synthesizer resources.
    engineMutex.lock();
    engineMutex.unlock();

    return (jint) result;
}

static jobjectArray
com_android_tts_compat_SynthProxy_getLanguage(JNIEnv *env, jobject thiz, jlong jniData)
{
    SynthProxyJniStorage* pSynthData = getSynthData(jniData);
    if (pSynthData == NULL) {
        return NULL;
    }

    if (pSynthData->mEngine) {
        size_t bufSize = 100;
        char lang[bufSize];
        char country[bufSize];
        char variant[bufSize];
        memset(lang, 0, bufSize);
        memset(country, 0, bufSize);
        memset(variant, 0, bufSize);
        jobjectArray retLocale = (jobjectArray)env->NewObjectArray(3,
                env->FindClass("java/lang/String"), env->NewStringUTF(""));

        android_tts_engine_t *engine = pSynthData->mEngine;
        engine->funcs->getLanguage(engine, lang, country, variant);
        env->SetObjectArrayElement(retLocale, 0, env->NewStringUTF(lang));
        env->SetObjectArrayElement(retLocale, 1, env->NewStringUTF(country));
        env->SetObjectArrayElement(retLocale, 2, env->NewStringUTF(variant));
        return retLocale;
    } else {
        return NULL;
    }
}


// Dalvik VM type signatures
static JNINativeMethod gMethods[] = {
    {   "native_stop",
        "(J)I",
        (void*)com_android_tts_compat_SynthProxy_stop
    },
    {   "native_stopSync",
        "(J)I",
        (void*)com_android_tts_compat_SynthProxy_stopSync
    },
    {   "native_speak",
        "(JLjava/lang/String;Landroid/speech/tts/SynthesisCallback;)I",
        (void*)com_android_tts_compat_SynthProxy_speak
    },
    {   "native_isLanguageAvailable",
        "(JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;)I",
        (void*)com_android_tts_compat_SynthProxy_isLanguageAvailable
    },
    {   "native_setLanguage",
        "(JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;)I",
        (void*)com_android_tts_compat_SynthProxy_setLanguage
    },
    {   "native_loadLanguage",
        "(JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;)I",
        (void*)com_android_tts_compat_SynthProxy_loadLanguage
    },
    {   "native_setProperty",
        "(JLjava/lang/String;Ljava/lang/String;)I",
        (void*)com_android_tts_compat_SynthProxy_setProperty
    },
    {   "native_getLanguage",
        "(J)[Ljava/lang/String;",
        (void*)com_android_tts_compat_SynthProxy_getLanguage
    },
    {   "native_shutdown",
        "(J)V",
        (void*)com_android_tts_compat_SynthProxy_shutdown
    },
    {   "native_setup",
        "(Ljava/lang/String;Ljava/lang/String;)J",
        (void*)com_android_tts_compat_SynthProxy_native_setup
    },
    {   "native_setLowShelf",
        "(ZFFFF)I",
        (void*)com_android_tts_compat_SynthProxy_setLowShelf
    },
    {   "native_finalize",
        "(J)V",
        (void*)com_android_tts_compat_SynthProxy_native_finalize
    }
};

jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
    JNIEnv* env = NULL;

    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        ALOGE("ERROR: GetEnv failed\n");
        return -1;
    }
    assert(env != NULL);

    jclass classSynthesisRequest = env->FindClass(
            "android/speech/tts/SynthesisCallback");
    if (classSynthesisRequest == NULL) {
        return -1;
    }

    synthesisRequest_start = env->GetMethodID(classSynthesisRequest,
            "start", "(III)I");
    if (synthesisRequest_start == NULL) {
        return -1;
    }

    synthesisRequest_audioAvailable = env->GetMethodID(classSynthesisRequest,
            "audioAvailable", "([BII)I");
    if (synthesisRequest_audioAvailable == NULL) {
        return -1;
    }

    synthesisRequest_done = env->GetMethodID(classSynthesisRequest,
            "done", "()I");
    if (synthesisRequest_done == NULL) {
        return -1;
    }

    if (jniRegisterNativeMethods(
            env, "com/android/tts/compat/SynthProxy", gMethods, NELEM(gMethods)) < 0) {
        return -1;
    }

    /* success -- return valid version number */
    return JNI_VERSION_1_4;
}