/*
 * Copyright (C) 2021 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.
 */
#include "game-text-input/gametextinput.h"

#include <android/log.h>
#include <jni.h>
#include <stdlib.h>
#include <string.h>

#include <algorithm>
#include <memory>
#include <vector>

#define LOG_TAG "GameTextInput"

static constexpr int32_t DEFAULT_MAX_STRING_SIZE = 1 << 16;

// Cache of field ids in the Java GameTextInputState class
struct StateClassInfo {
    jfieldID text;
    jfieldID selectionStart;
    jfieldID selectionEnd;
    jfieldID composingRegionStart;
    jfieldID composingRegionEnd;
};

// Main GameTextInput object.
struct GameTextInput {
   public:
    GameTextInput(JNIEnv *env, uint32_t max_string_size);
    ~GameTextInput();
    void setState(const GameTextInputState &state);
    const GameTextInputState &getState() const { return currentState_; }
    void setInputConnection(jobject inputConnection);
    void processEvent(jobject textInputEvent);
    void showIme(uint32_t flags);
    void hideIme(uint32_t flags);
    void restartInput();
    void setEventCallback(GameTextInputEventCallback callback, void *context);
    jobject stateToJava(const GameTextInputState &state) const;
    void stateFromJava(jobject textInputEvent,
                       GameTextInputGetStateCallback callback,
                       void *context) const;
    void setImeInsetsCallback(GameTextInputImeInsetsCallback callback,
                              void *context);
    void processImeInsets(const ARect *insets);
    const ARect &getImeInsets() const { return currentInsets_; }

   private:
    // Copy string and set other fields
    void setStateInner(const GameTextInputState &state);
    static void processCallback(void *context, const GameTextInputState *state);
    JNIEnv *env_ = nullptr;
    // Cached at initialization from
    // com/google/androidgamesdk/gametextinput/State.
    jclass stateJavaClass_ = nullptr;
    // The latest text input update.
    GameTextInputState currentState_ = {};
    // An instance of gametextinput.InputConnection.
    jclass inputConnectionClass_ = nullptr;
    jobject inputConnection_ = nullptr;
    jmethodID inputConnectionSetStateMethod_;
    jmethodID setSoftKeyboardActiveMethod_;
    jmethodID restartInputMethod_;
    void (*eventCallback_)(void *context,
                           const struct GameTextInputState *state) = nullptr;
    void *eventCallbackContext_ = nullptr;
    void (*insetsCallback_)(void *context,
                            const struct ARect *insets) = nullptr;
    ARect currentInsets_ = {};
    void *insetsCallbackContext_ = nullptr;
    StateClassInfo stateClassInfo_ = {};
    // Constant-sized buffer used to store state text.
    std::vector<char> stateStringBuffer_;
};

std::unique_ptr<GameTextInput> s_gameTextInput;

extern "C" {

///////////////////////////////////////////////////////////
/// GameTextInputState C Functions
///////////////////////////////////////////////////////////

// Convert to a Java structure.
jobject currentState_toJava(const GameTextInput *gameTextInput,
                            const GameTextInputState *state) {
    if (state == nullptr) return NULL;
    return gameTextInput->stateToJava(*state);
}

// Convert from Java structure.
void currentState_fromJava(const GameTextInput *gameTextInput,
                           jobject textInputEvent,
                           GameTextInputGetStateCallback callback,
                           void *context) {
    gameTextInput->stateFromJava(textInputEvent, callback, context);
}

///////////////////////////////////////////////////////////
/// GameTextInput C Functions
///////////////////////////////////////////////////////////

struct GameTextInput *GameTextInput_init(JNIEnv *env,
                                         uint32_t max_string_size) {
    if (s_gameTextInput.get() != nullptr) {
        __android_log_print(ANDROID_LOG_WARN, LOG_TAG,
                            "Warning: called GameTextInput_init twice without "
                            "calling GameTextInput_destroy");
        return s_gameTextInput.get();
    }
    // Don't use make_unique, for C++11 compatibility
    s_gameTextInput =
        std::unique_ptr<GameTextInput>(new GameTextInput(env, max_string_size));
    return s_gameTextInput.get();
}

void GameTextInput_destroy(GameTextInput *input) {
    if (input == nullptr || s_gameTextInput.get() == nullptr) return;
    s_gameTextInput.reset();
}

void GameTextInput_setState(GameTextInput *input,
                            const GameTextInputState *state) {
    if (state == nullptr) return;
    input->setState(*state);
}

void GameTextInput_getState(GameTextInput *input,
                            GameTextInputGetStateCallback callback,
                            void *context) {
    callback(context, &input->getState());
}

void GameTextInput_setInputConnection(GameTextInput *input,
                                      jobject inputConnection) {
    input->setInputConnection(inputConnection);
}

void GameTextInput_processEvent(GameTextInput *input, jobject textInputEvent) {
    input->processEvent(textInputEvent);
}

void GameTextInput_processImeInsets(GameTextInput *input, const ARect *insets) {
    input->processImeInsets(insets);
}

void GameTextInput_showIme(struct GameTextInput *input, uint32_t flags) {
    input->showIme(flags);
}

void GameTextInput_hideIme(struct GameTextInput *input, uint32_t flags) {
    input->hideIme(flags);
}

void GameTextInput_restartInput(struct GameTextInput *input) {
    input->restartInput();
}

void GameTextInput_setEventCallback(struct GameTextInput *input,
                                    GameTextInputEventCallback callback,
                                    void *context) {
    input->setEventCallback(callback, context);
}

void GameTextInput_setImeInsetsCallback(struct GameTextInput *input,
                                        GameTextInputImeInsetsCallback callback,
                                        void *context) {
    input->setImeInsetsCallback(callback, context);
}

void GameTextInput_getImeInsets(const GameTextInput *input, ARect *insets) {
    *insets = input->getImeInsets();
}

}  // extern "C"

///////////////////////////////////////////////////////////
/// GameTextInput C++ class Implementation
///////////////////////////////////////////////////////////

GameTextInput::GameTextInput(JNIEnv *env, uint32_t max_string_size)
    : env_(env),
      stateStringBuffer_(max_string_size == 0 ? DEFAULT_MAX_STRING_SIZE
                                              : max_string_size) {
    stateJavaClass_ = (jclass)env_->NewGlobalRef(
        env_->FindClass("com/google/androidgamesdk/gametextinput/State"));
    inputConnectionClass_ = (jclass)env_->NewGlobalRef(env_->FindClass(
        "com/google/androidgamesdk/gametextinput/InputConnection"));
    inputConnectionSetStateMethod_ =
        env_->GetMethodID(inputConnectionClass_, "setState",
                          "(Lcom/google/androidgamesdk/gametextinput/State;)V");
    setSoftKeyboardActiveMethod_ = env_->GetMethodID(
        inputConnectionClass_, "setSoftKeyboardActive", "(ZI)V");
    restartInputMethod_ =
        env_->GetMethodID(inputConnectionClass_, "restartInput", "()V");

    stateClassInfo_.text =
        env_->GetFieldID(stateJavaClass_, "text", "Ljava/lang/String;");
    stateClassInfo_.selectionStart =
        env_->GetFieldID(stateJavaClass_, "selectionStart", "I");
    stateClassInfo_.selectionEnd =
        env_->GetFieldID(stateJavaClass_, "selectionEnd", "I");
    stateClassInfo_.composingRegionStart =
        env_->GetFieldID(stateJavaClass_, "composingRegionStart", "I");
    stateClassInfo_.composingRegionEnd =
        env_->GetFieldID(stateJavaClass_, "composingRegionEnd", "I");
}

GameTextInput::~GameTextInput() {
    if (stateJavaClass_ != NULL) {
        env_->DeleteGlobalRef(stateJavaClass_);
        stateJavaClass_ = NULL;
    }
    if (inputConnectionClass_ != NULL) {
        env_->DeleteGlobalRef(inputConnectionClass_);
        inputConnectionClass_ = NULL;
    }
    if (inputConnection_ != NULL) {
        env_->DeleteGlobalRef(inputConnection_);
        inputConnection_ = NULL;
    }
}

void GameTextInput::setState(const GameTextInputState &state) {
    if (inputConnection_ == nullptr) return;
    jobject jstate = stateToJava(state);
    env_->CallVoidMethod(inputConnection_, inputConnectionSetStateMethod_,
                         jstate);
    env_->DeleteLocalRef(jstate);
    setStateInner(state);
}

void GameTextInput::setStateInner(const GameTextInputState &state) {
    // Check if we're setting using our own string (other parts may be
    // different)
    if (state.text_UTF8 == currentState_.text_UTF8) {
        currentState_ = state;
        return;
    }
    // Otherwise, copy across the string.
    auto bytes_needed =
        std::min(static_cast<uint32_t>(state.text_length + 1),
                 static_cast<uint32_t>(stateStringBuffer_.size()));
    currentState_.text_UTF8 = stateStringBuffer_.data();
    std::copy(state.text_UTF8, state.text_UTF8 + bytes_needed - 1,
              stateStringBuffer_.data());
    currentState_.text_length = state.text_length;
    currentState_.selection = state.selection;
    currentState_.composingRegion = state.composingRegion;
    stateStringBuffer_[bytes_needed - 1] = 0;
}

void GameTextInput::setInputConnection(jobject inputConnection) {
    if (inputConnection_ != NULL) {
        env_->DeleteGlobalRef(inputConnection_);
    }
    inputConnection_ = env_->NewGlobalRef(inputConnection);
}

/*static*/ void GameTextInput::processCallback(
    void *context, const GameTextInputState *state) {
    auto thiz = static_cast<GameTextInput *>(context);
    if (state != nullptr) thiz->setStateInner(*state);
}

void GameTextInput::processEvent(jobject textInputEvent) {
    stateFromJava(textInputEvent, processCallback, this);
    if (eventCallback_) {
        eventCallback_(eventCallbackContext_, &currentState_);
    }
}

void GameTextInput::showIme(uint32_t flags) {
    if (inputConnection_ == nullptr) return;
    env_->CallVoidMethod(inputConnection_, setSoftKeyboardActiveMethod_, true,
                         flags);
}

void GameTextInput::setEventCallback(GameTextInputEventCallback callback,
                                     void *context) {
    eventCallback_ = callback;
    eventCallbackContext_ = context;
}

void GameTextInput::setImeInsetsCallback(
    GameTextInputImeInsetsCallback callback, void *context) {
    insetsCallback_ = callback;
    insetsCallbackContext_ = context;
}

void GameTextInput::processImeInsets(const ARect *insets) {
    currentInsets_ = *insets;
    if (insetsCallback_) {
        insetsCallback_(insetsCallbackContext_, &currentInsets_);
    }
}

void GameTextInput::hideIme(uint32_t flags) {
    if (inputConnection_ == nullptr) return;
    env_->CallVoidMethod(inputConnection_, setSoftKeyboardActiveMethod_, false,
                         flags);
}

void GameTextInput::restartInput() {
    if (inputConnection_ == nullptr) return;
    env_->CallVoidMethod(inputConnection_, restartInputMethod_, false);
}

jobject GameTextInput::stateToJava(const GameTextInputState &state) const {
    static jmethodID constructor = nullptr;
    if (constructor == nullptr) {
        constructor = env_->GetMethodID(stateJavaClass_, "<init>",
                                        "(Ljava/lang/String;IIII)V");
        if (constructor == nullptr) {
            __android_log_print(ANDROID_LOG_ERROR, LOG_TAG,
                                "Can't find gametextinput.State constructor");
            return nullptr;
        }
    }
    const char *text = state.text_UTF8;
    if (text == nullptr) {
        static char empty_string[] = "";
        text = empty_string;
    }
    // Note that this expects 'modified' UTF-8 which is not the same as UTF-8
    // https://en.wikipedia.org/wiki/UTF-8#Modified_UTF-8
    jstring jtext = env_->NewStringUTF(text);
    jobject jobj =
        env_->NewObject(stateJavaClass_, constructor, jtext,
                        state.selection.start, state.selection.end,
                        state.composingRegion.start, state.composingRegion.end);
    env_->DeleteLocalRef(jtext);
    return jobj;
}

void GameTextInput::stateFromJava(jobject textInputEvent,
                                  GameTextInputGetStateCallback callback,
                                  void *context) const {
    jstring text =
        (jstring)env_->GetObjectField(textInputEvent, stateClassInfo_.text);
    // Note this is 'modified' UTF-8, not true UTF-8. It has no NULLs in it,
    // except at the end. It's actually not specified whether the value returned
    // by GetStringUTFChars includes a null at the end, but it *seems to* on
    // Android.
    const char *text_chars = env_->GetStringUTFChars(text, NULL);
    int text_len = env_->GetStringUTFLength(
        text);  // Length in bytes, *not* including the null.
    int selectionStart =
        env_->GetIntField(textInputEvent, stateClassInfo_.selectionStart);
    int selectionEnd =
        env_->GetIntField(textInputEvent, stateClassInfo_.selectionEnd);
    int composingRegionStart =
        env_->GetIntField(textInputEvent, stateClassInfo_.composingRegionStart);
    int composingRegionEnd =
        env_->GetIntField(textInputEvent, stateClassInfo_.composingRegionEnd);
    GameTextInputState state{text_chars,
                             text_len,
                             {selectionStart, selectionEnd},
                             {composingRegionStart, composingRegionEnd}};
    callback(context, &state);
    env_->ReleaseStringUTFChars(text, text_chars);
    env_->DeleteLocalRef(text);
}
