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

#define LOG_TAG "InputChannel-JNI"

#include "android-base/stringprintf.h"
#include <nativehelper/JNIHelp.h>
#include "nativehelper/scoped_utf_chars.h"
#include <android_runtime/AndroidRuntime.h>
#include <binder/Parcel.h>
#include <utils/Log.h>
#include <input/InputTransport.h>
#include "android_view_InputChannel.h"
#include "android_os_Parcel.h"
#include "android_util_Binder.h"

#include "core_jni_helpers.h"

namespace android {

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

static struct {
    jclass clazz;

    jmethodID mCtor;
    jmethodID mSetNativeInputChannel;

    jfieldID mPtr;   // native object attached to the DVM InputChannel
} gInputChannelClassInfo;

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

class NativeInputChannel {
public:
    explicit NativeInputChannel(std::unique_ptr<InputChannel> inputChannel);
    ~NativeInputChannel();

    inline std::shared_ptr<InputChannel> getInputChannel() { return mInputChannel; }

    void setDisposeCallback(InputChannelObjDisposeCallback callback, void* data);
    void dispose(JNIEnv* env, jobject obj);

private:
    std::shared_ptr<InputChannel> mInputChannel;
    InputChannelObjDisposeCallback mDisposeCallback;
    void* mDisposeData;
};

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

NativeInputChannel::NativeInputChannel(std::unique_ptr<InputChannel> inputChannel)
      : mInputChannel(std::move(inputChannel)), mDisposeCallback(nullptr) {}

NativeInputChannel::~NativeInputChannel() {
}

void NativeInputChannel::setDisposeCallback(InputChannelObjDisposeCallback callback, void* data) {
    mDisposeCallback = callback;
    mDisposeData = data;
}

void NativeInputChannel::dispose(JNIEnv* env, jobject obj) {
    if (!mInputChannel) {
        return;
    }

    if (mDisposeCallback) {
        mDisposeCallback(env, obj, mInputChannel, mDisposeData);
        mDisposeCallback = nullptr;
        mDisposeData = nullptr;
    }
    mInputChannel.reset();
}

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

static NativeInputChannel* android_view_InputChannel_getNativeInputChannel(JNIEnv* env,
        jobject inputChannelObj) {
    jlong longPtr = env->GetLongField(inputChannelObj, gInputChannelClassInfo.mPtr);
    return reinterpret_cast<NativeInputChannel*>(longPtr);
}

std::shared_ptr<InputChannel> android_view_InputChannel_getInputChannel(JNIEnv* env,
                                                                        jobject inputChannelObj) {
    NativeInputChannel* nativeInputChannel =
            android_view_InputChannel_getNativeInputChannel(env, inputChannelObj);
    return nativeInputChannel != nullptr ? nativeInputChannel->getInputChannel() : nullptr;
}

void android_view_InputChannel_setDisposeCallback(JNIEnv* env, jobject inputChannelObj,
        InputChannelObjDisposeCallback callback, void* data) {
    NativeInputChannel* nativeInputChannel =
            android_view_InputChannel_getNativeInputChannel(env, inputChannelObj);
    if (!nativeInputChannel || !nativeInputChannel->getInputChannel()) {
        ALOGW("Cannot set dispose callback because input channel object has not been initialized.");
    } else {
        nativeInputChannel->setDisposeCallback(callback, data);
    }
}

static jlong android_view_InputChannel_createInputChannel(
        JNIEnv* env, std::unique_ptr<InputChannel> inputChannel) {
    std::unique_ptr<NativeInputChannel> nativeInputChannel =
            std::make_unique<NativeInputChannel>(std::move(inputChannel));

    return reinterpret_cast<jlong>(nativeInputChannel.release());
}

jobject android_view_InputChannel_createJavaObject(JNIEnv* env,
                                                   std::unique_ptr<InputChannel> inputChannel) {
    std::string name = inputChannel->getName();
    jlong ptr = android_view_InputChannel_createInputChannel(env, std::move(inputChannel));
    jobject javaInputChannel =
            env->NewObject(gInputChannelClassInfo.clazz, gInputChannelClassInfo.mCtor);
    if (!javaInputChannel) {
        ALOGE("Failed to create a Java InputChannel for channel %s.", name.c_str());
        return nullptr;
    }

    env->CallVoidMethod(javaInputChannel, gInputChannelClassInfo.mSetNativeInputChannel, ptr);
    if (env->ExceptionOccurred()) {
        ALOGE("Failed to set native ptr to the Java InputChannel for channel %s.",
              inputChannel->getName().c_str());
        return nullptr;
    }
    return javaInputChannel;
}

static jlongArray android_view_InputChannel_nativeOpenInputChannelPair(JNIEnv* env,
        jclass clazz, jstring nameObj) {
    ScopedUtfChars nameChars(env, nameObj);
    std::string name = nameChars.c_str();

    std::unique_ptr<InputChannel> serverChannel;
    std::unique_ptr<InputChannel> clientChannel;
    status_t result = InputChannel::openInputChannelPair(name, serverChannel, clientChannel);

    if (result) {
        std::string message = android::base::StringPrintf(
                "Could not open input channel pair : %s", strerror(-result));
        jniThrowRuntimeException(env, message.c_str());
        return nullptr;
    }

    jlongArray channelPair = env->NewLongArray(2);
    if (channelPair == nullptr) {
        return nullptr;
    }

    jlong* outArray = env->GetLongArrayElements(channelPair, 0);
    outArray[0] = android_view_InputChannel_createInputChannel(env, std::move(serverChannel));
    if (env->ExceptionCheck()) {
        return nullptr;
    }

    outArray[1] = android_view_InputChannel_createInputChannel(env, std::move(clientChannel));
    if (env->ExceptionCheck()) {
        return nullptr;
    }
    env->ReleaseLongArrayElements(channelPair, outArray, 0);

    return channelPair;
}

static void InputChannel_nativeDestroy(void *rawInputChannel) {
    NativeInputChannel* nativeInputChannel =
            reinterpret_cast<NativeInputChannel *>(rawInputChannel);
    if (nativeInputChannel) {
        delete nativeInputChannel;
    }
}

static jlong android_view_InputChannel_getNativeFinalizer(JNIEnv* env, jobject obj) {
    return static_cast<jlong>(reinterpret_cast<uintptr_t>(&InputChannel_nativeDestroy));
}

static void android_view_InputChannel_nativeDispose(JNIEnv* env, jobject obj, jlong channel) {
    NativeInputChannel* nativeInputChannel =
            reinterpret_cast<NativeInputChannel*>(channel);

    if (nativeInputChannel) {
        nativeInputChannel->dispose(env, obj);
    }
}

static jlong android_view_InputChannel_nativeReadFromParcel(JNIEnv* env, jobject obj,
        jobject parcelObj) {
    Parcel* parcel = parcelForJavaObject(env, parcelObj);
    if (parcel) {
        bool isInitialized = parcel->readInt32();
        if (isInitialized) {
            std::unique_ptr<InputChannel> inputChannel = std::make_unique<InputChannel>();
            inputChannel->readFromParcel(parcel);
            NativeInputChannel* nativeInputChannel =
                    new NativeInputChannel(std::move(inputChannel));
            return reinterpret_cast<jlong>(nativeInputChannel);
        }
    }
    return 0;
}

static void android_view_InputChannel_nativeWriteToParcel(JNIEnv* env, jobject obj,
        jobject parcelObj, jlong channel) {
    Parcel* parcel = parcelForJavaObject(env, parcelObj);
    if (parcel == nullptr) {
        ALOGE("Could not obtain parcel for Java object");
        return;
    }
    NativeInputChannel* nativeInputChannel =
                reinterpret_cast<NativeInputChannel*>(channel);

    if (!nativeInputChannel || !nativeInputChannel->getInputChannel()) {
        parcel->writeInt32(0); // not initialized
        return;
    }
    parcel->writeInt32(1); // initialized
    nativeInputChannel->getInputChannel()->writeToParcel(parcel);
}

static jstring android_view_InputChannel_nativeGetName(JNIEnv* env, jobject obj, jlong channel) {
    NativeInputChannel* nativeInputChannel =
                reinterpret_cast<NativeInputChannel*>(channel);
    if (!nativeInputChannel || !nativeInputChannel->getInputChannel()) {
        return nullptr;
    }

    jstring name = env->NewStringUTF(nativeInputChannel->getInputChannel()->getName().c_str());
    return name;
}

static jlong android_view_InputChannel_nativeDup(JNIEnv* env, jobject obj, jlong channel) {
    NativeInputChannel* nativeInputChannel =
                reinterpret_cast<NativeInputChannel*>(channel);

    if (nativeInputChannel == nullptr) {
        jniThrowRuntimeException(env, "InputChannel has no valid NativeInputChannel");
        return 0;
    }

    std::shared_ptr<InputChannel> inputChannel = nativeInputChannel->getInputChannel();
    if (inputChannel == nullptr) {
        jniThrowRuntimeException(env, "NativeInputChannel has no corresponding InputChannel");
        return 0;
    }

    std::unique_ptr<InputChannel> dupInputChannel = inputChannel->dup();
    if (dupInputChannel == nullptr) {
        std::string message = android::base::StringPrintf(
                "Could not duplicate input channel %s", inputChannel->getName().c_str());
        jniThrowRuntimeException(env, message.c_str());
    }
    return reinterpret_cast<jlong>(new NativeInputChannel(std::move(dupInputChannel)));
}

static jobject android_view_InputChannel_nativeGetToken(JNIEnv* env, jobject obj, jlong channel) {
    NativeInputChannel* nativeInputChannel =
                reinterpret_cast<NativeInputChannel*>(channel);
    if (nativeInputChannel && nativeInputChannel->getInputChannel()) {
        return javaObjectForIBinder(env,
                nativeInputChannel->getInputChannel()->getConnectionToken());
    }
    return 0;
}

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

static const JNINativeMethod gInputChannelMethods[] = {
    /* name, signature, funcPtr */
    { "nativeOpenInputChannelPair", "(Ljava/lang/String;)[J",
            (void*)android_view_InputChannel_nativeOpenInputChannelPair },
    { "nativeGetFinalizer", "()J",
            (void*)android_view_InputChannel_getNativeFinalizer },
    { "nativeDispose", "(J)V",
            (void*)android_view_InputChannel_nativeDispose },
    { "nativeReadFromParcel", "(Landroid/os/Parcel;)J",
            (void*)android_view_InputChannel_nativeReadFromParcel },
    { "nativeWriteToParcel", "(Landroid/os/Parcel;J)V",
            (void*)android_view_InputChannel_nativeWriteToParcel },
    { "nativeGetName", "(J)Ljava/lang/String;",
            (void*)android_view_InputChannel_nativeGetName },
    { "nativeDup", "(J)J",
            (void*)android_view_InputChannel_nativeDup },
    { "nativeGetToken", "(J)Landroid/os/IBinder;",
            (void*)android_view_InputChannel_nativeGetToken },
};

int register_android_view_InputChannel(JNIEnv* env) {
    int res = RegisterMethodsOrDie(env, "android/view/InputChannel", gInputChannelMethods,
                                   NELEM(gInputChannelMethods));

    jclass clazz = FindClassOrDie(env, "android/view/InputChannel");
    gInputChannelClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);

    gInputChannelClassInfo.mCtor =
            GetMethodIDOrDie(env, gInputChannelClassInfo.clazz, "<init>", "()V");
    gInputChannelClassInfo.mSetNativeInputChannel =
            GetMethodIDOrDie(env, gInputChannelClassInfo.clazz, "setNativeInputChannel", "(J)V");

    gInputChannelClassInfo.mPtr = GetFieldIDOrDie(env, gInputChannelClassInfo.clazz, "mPtr", "J");

    return res;
}

} // namespace android
