/*
 * Copyright (C) 2022 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 "GameActivityEvents.h"

#include <sys/system_properties.h>

#include <string>

#include "GameActivityLog.h"

// TODO(b/187147166): these functions were extracted from the Game SDK
// (gamesdk/src/common/system_utils.h). system_utils.h/cpp should be used
// instead.
namespace {

std::string getSystemPropViaGet(const char *key,
                                const char *default_value = "") {
    char buffer[PROP_VALUE_MAX + 1] = "";  // +1 for terminator
    int bufferLen = __system_property_get(key, buffer);
    if (bufferLen > 0)
        return buffer;
    else
        return "";
}

std::string GetSystemProp(const char *key, const char *default_value = "") {
    return getSystemPropViaGet(key, default_value);
}

int GetSystemPropAsInt(const char *key, int default_value = 0) {
    std::string prop = GetSystemProp(key);
    return prop == "" ? default_value : strtoll(prop.c_str(), nullptr, 10);
}

}  // anonymous namespace

#ifndef NELEM
#define NELEM(x) ((int)(sizeof(x) / sizeof((x)[0])))
#endif

static bool enabledAxes[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT] = {
    /* AMOTION_EVENT_AXIS_X */ true,
    /* AMOTION_EVENT_AXIS_Y */ true,
    // Disable all other axes by default (they can be enabled using
    // `GameActivityPointerAxes_enableAxis`).
    false};

extern "C" void GameActivityPointerAxes_enableAxis(int32_t axis) {
    if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
        return;
    }

    enabledAxes[axis] = true;
}

float GameActivityPointerAxes_getAxisValue(
    const GameActivityPointerAxes *pointerInfo, int32_t axis) {
    if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
        return 0;
    }

    if (!enabledAxes[axis]) {
        ALOGW("Axis %d must be enabled before it can be accessed.", axis);
        return 0;
    }

    return pointerInfo->axisValues[axis];
}

extern "C" void GameActivityPointerAxes_disableAxis(int32_t axis) {
    if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
        return;
    }

    enabledAxes[axis] = false;
}

float GameActivityMotionEvent_getHistoricalAxisValue(
    const GameActivityMotionEvent *event, int axis, int pointerIndex,
    int historyPos) {
    if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
        ALOGE("Invalid axis %d", axis);
        return -1;
    }
    if (pointerIndex < 0 || pointerIndex >= event->pointerCount) {
        ALOGE("Invalid pointer index %d", pointerIndex);
        return -1;
    }
    if (historyPos < 0 || historyPos >= event->historySize) {
        ALOGE("Invalid history index %d", historyPos);
        return -1;
    }
    if (!enabledAxes[axis]) {
        ALOGW("Axis %d must be enabled before it can be accessed.", axis);
        return 0;
    }

    int pointerOffset = pointerIndex * GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT;
    int historyValuesOffset = historyPos * event->pointerCount *
                              GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT;
    return event
        ->historicalAxisValues[historyValuesOffset + pointerOffset + axis];
}

static struct {
    jmethodID getDeviceId;
    jmethodID getSource;
    jmethodID getAction;

    jmethodID getEventTime;
    jmethodID getDownTime;

    jmethodID getFlags;
    jmethodID getMetaState;

    jmethodID getActionButton;
    jmethodID getButtonState;
    jmethodID getClassification;
    jmethodID getEdgeFlags;

    jmethodID getHistorySize;
    jmethodID getHistoricalEventTime;

    jmethodID getPointerCount;
    jmethodID getPointerId;

    jmethodID getToolType;

    jmethodID getRawX;
    jmethodID getRawY;
    jmethodID getXPrecision;
    jmethodID getYPrecision;
    jmethodID getAxisValue;

    jmethodID getHistoricalAxisValue;
} gMotionEventClassInfo;

extern "C" void GameActivityMotionEvent_destroy(
    GameActivityMotionEvent *c_event) {
    delete c_event->historicalAxisValues;
    delete c_event->historicalEventTimesMillis;
    delete c_event->historicalEventTimesNanos;
}

extern "C" void GameActivityMotionEvent_fromJava(
    JNIEnv *env, jobject motionEvent, GameActivityMotionEvent *out_event) {
    static bool gMotionEventClassInfoInitialized = false;
    if (!gMotionEventClassInfoInitialized) {
        int sdkVersion = GetSystemPropAsInt("ro.build.version.sdk");
        gMotionEventClassInfo = {0};
        jclass motionEventClass = env->FindClass("android/view/MotionEvent");
        gMotionEventClassInfo.getDeviceId =
            env->GetMethodID(motionEventClass, "getDeviceId", "()I");
        gMotionEventClassInfo.getSource =
            env->GetMethodID(motionEventClass, "getSource", "()I");
        gMotionEventClassInfo.getAction =
            env->GetMethodID(motionEventClass, "getAction", "()I");
        gMotionEventClassInfo.getEventTime =
            env->GetMethodID(motionEventClass, "getEventTime", "()J");
        gMotionEventClassInfo.getDownTime =
            env->GetMethodID(motionEventClass, "getDownTime", "()J");
        gMotionEventClassInfo.getFlags =
            env->GetMethodID(motionEventClass, "getFlags", "()I");
        gMotionEventClassInfo.getMetaState =
            env->GetMethodID(motionEventClass, "getMetaState", "()I");
        if (sdkVersion >= 23) {
            gMotionEventClassInfo.getActionButton =
                env->GetMethodID(motionEventClass, "getActionButton", "()I");
        }
        if (sdkVersion >= 14) {
            gMotionEventClassInfo.getButtonState =
                env->GetMethodID(motionEventClass, "getButtonState", "()I");
        }
        if (sdkVersion >= 29) {
            gMotionEventClassInfo.getClassification =
                env->GetMethodID(motionEventClass, "getClassification", "()I");
        }
        gMotionEventClassInfo.getEdgeFlags =
            env->GetMethodID(motionEventClass, "getEdgeFlags", "()I");

        gMotionEventClassInfo.getHistorySize =
            env->GetMethodID(motionEventClass, "getHistorySize", "()I");
        gMotionEventClassInfo.getHistoricalEventTime = env->GetMethodID(
            motionEventClass, "getHistoricalEventTime", "(I)J");

        gMotionEventClassInfo.getPointerCount =
            env->GetMethodID(motionEventClass, "getPointerCount", "()I");
        gMotionEventClassInfo.getPointerId =
            env->GetMethodID(motionEventClass, "getPointerId", "(I)I");
        gMotionEventClassInfo.getToolType =
            env->GetMethodID(motionEventClass, "getToolType", "(I)I");
        if (sdkVersion >= 29) {
            gMotionEventClassInfo.getRawX =
                env->GetMethodID(motionEventClass, "getRawX", "(I)F");
            gMotionEventClassInfo.getRawY =
                env->GetMethodID(motionEventClass, "getRawY", "(I)F");
        }
        gMotionEventClassInfo.getXPrecision =
            env->GetMethodID(motionEventClass, "getXPrecision", "()F");
        gMotionEventClassInfo.getYPrecision =
            env->GetMethodID(motionEventClass, "getYPrecision", "()F");
        gMotionEventClassInfo.getAxisValue =
            env->GetMethodID(motionEventClass, "getAxisValue", "(II)F");

        gMotionEventClassInfo.getHistoricalAxisValue = env->GetMethodID(
            motionEventClass, "getHistoricalAxisValue", "(III)F");
        gMotionEventClassInfoInitialized = true;
    }

    int pointerCount =
        env->CallIntMethod(motionEvent, gMotionEventClassInfo.getPointerCount);
    pointerCount =
        std::min(pointerCount, GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT);
    out_event->pointerCount = pointerCount;
    for (int i = 0; i < pointerCount; ++i) {
        out_event->pointers[i] = {
            /*id=*/env->CallIntMethod(motionEvent,
                                      gMotionEventClassInfo.getPointerId, i),
            /*toolType=*/
            env->CallIntMethod(motionEvent, gMotionEventClassInfo.getToolType,
                               i),
            /*axisValues=*/{0},
            /*rawX=*/gMotionEventClassInfo.getRawX
                ? env->CallFloatMethod(motionEvent,
                                       gMotionEventClassInfo.getRawX, i)
                : 0,
            /*rawY=*/gMotionEventClassInfo.getRawY
                ? env->CallFloatMethod(motionEvent,
                                       gMotionEventClassInfo.getRawY, i)
                : 0,
        };

        for (int axisIndex = 0;
             axisIndex < GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT; ++axisIndex) {
            if (enabledAxes[axisIndex]) {
                out_event->pointers[i].axisValues[axisIndex] =
                    env->CallFloatMethod(motionEvent,
                                         gMotionEventClassInfo.getAxisValue,
                                         axisIndex, i);
            }
        }
    }

    int historySize =
        env->CallIntMethod(motionEvent, gMotionEventClassInfo.getHistorySize);
    out_event->historySize = historySize;
    out_event->historicalAxisValues =
        new float[historySize * pointerCount *
                  GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT];
    out_event->historicalEventTimesMillis = new int64_t[historySize];
    out_event->historicalEventTimesNanos = new int64_t[historySize];

    for (int historyIndex = 0; historyIndex < historySize; historyIndex++) {
        out_event->historicalEventTimesMillis[historyIndex] =
            env->CallLongMethod(motionEvent,
                                gMotionEventClassInfo.getHistoricalEventTime,
                                historyIndex);
        out_event->historicalEventTimesNanos[historyIndex] =
            out_event->historicalEventTimesMillis[historyIndex] * 1000000;
        for (int i = 0; i < pointerCount; ++i) {
            int pointerOffset = i * GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT;
            int historyAxisOffset = historyIndex * pointerCount *
                                    GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT;
            float *axisValues =
                &out_event
                     ->historicalAxisValues[historyAxisOffset + pointerOffset];
            for (int axisIndex = 0;
                 axisIndex < GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT;
                 ++axisIndex) {
                if (enabledAxes[axisIndex]) {
                    axisValues[axisIndex] = env->CallFloatMethod(
                        motionEvent,
                        gMotionEventClassInfo.getHistoricalAxisValue, axisIndex,
                        i, historyIndex);
                }
            }
        }
    }

    out_event->deviceId =
        env->CallIntMethod(motionEvent, gMotionEventClassInfo.getDeviceId);
    out_event->source =
        env->CallIntMethod(motionEvent, gMotionEventClassInfo.getSource);
    out_event->action =
        env->CallIntMethod(motionEvent, gMotionEventClassInfo.getAction);
    out_event->eventTime =
        env->CallLongMethod(motionEvent, gMotionEventClassInfo.getEventTime) *
        1000000;
    out_event->downTime =
        env->CallLongMethod(motionEvent, gMotionEventClassInfo.getDownTime) *
        1000000;
    out_event->flags =
        env->CallIntMethod(motionEvent, gMotionEventClassInfo.getFlags);
    out_event->metaState =
        env->CallIntMethod(motionEvent, gMotionEventClassInfo.getMetaState);
    out_event->actionButton =
        gMotionEventClassInfo.getActionButton
            ? env->CallIntMethod(motionEvent,
                                 gMotionEventClassInfo.getActionButton)
            : 0;
    out_event->buttonState =
        gMotionEventClassInfo.getButtonState
            ? env->CallIntMethod(motionEvent,
                                 gMotionEventClassInfo.getButtonState)
            : 0;
    out_event->classification =
        gMotionEventClassInfo.getClassification
            ? env->CallIntMethod(motionEvent,
                                 gMotionEventClassInfo.getClassification)
            : 0;
    out_event->edgeFlags =
        env->CallIntMethod(motionEvent, gMotionEventClassInfo.getEdgeFlags);
    out_event->precisionX =
        env->CallFloatMethod(motionEvent, gMotionEventClassInfo.getXPrecision);
    out_event->precisionY =
        env->CallFloatMethod(motionEvent, gMotionEventClassInfo.getYPrecision);
}

static struct {
    jmethodID getDeviceId;
    jmethodID getSource;
    jmethodID getAction;

    jmethodID getEventTime;
    jmethodID getDownTime;

    jmethodID getFlags;
    jmethodID getMetaState;

    jmethodID getModifiers;
    jmethodID getRepeatCount;
    jmethodID getKeyCode;
    jmethodID getScanCode;
    //jmethodID getUnicodeChar;
} gKeyEventClassInfo;

extern "C" void GameActivityKeyEvent_fromJava(JNIEnv *env, jobject keyEvent,
                                              GameActivityKeyEvent *out_event) {
    static bool gKeyEventClassInfoInitialized = false;
    if (!gKeyEventClassInfoInitialized) {
        int sdkVersion = GetSystemPropAsInt("ro.build.version.sdk");
        gKeyEventClassInfo = {0};
        jclass keyEventClass = env->FindClass("android/view/KeyEvent");
        gKeyEventClassInfo.getDeviceId =
            env->GetMethodID(keyEventClass, "getDeviceId", "()I");
        gKeyEventClassInfo.getSource =
            env->GetMethodID(keyEventClass, "getSource", "()I");
        gKeyEventClassInfo.getAction =
            env->GetMethodID(keyEventClass, "getAction", "()I");
        gKeyEventClassInfo.getEventTime =
            env->GetMethodID(keyEventClass, "getEventTime", "()J");
        gKeyEventClassInfo.getDownTime =
            env->GetMethodID(keyEventClass, "getDownTime", "()J");
        gKeyEventClassInfo.getFlags =
            env->GetMethodID(keyEventClass, "getFlags", "()I");
        gKeyEventClassInfo.getMetaState =
            env->GetMethodID(keyEventClass, "getMetaState", "()I");
        if (sdkVersion >= 13) {
            gKeyEventClassInfo.getModifiers =
                env->GetMethodID(keyEventClass, "getModifiers", "()I");
        }
        gKeyEventClassInfo.getRepeatCount =
            env->GetMethodID(keyEventClass, "getRepeatCount", "()I");
        gKeyEventClassInfo.getKeyCode =
            env->GetMethodID(keyEventClass, "getKeyCode", "()I");
        gKeyEventClassInfo.getScanCode =
            env->GetMethodID(keyEventClass, "getScanCode", "()I");
        //gKeyEventClassInfo.getUnicodeChar =
        //    env->GetMethodID(keyEventClass, "getUnicodeChar", "()I");

        gKeyEventClassInfoInitialized = true;
    }

    *out_event = {
        /*deviceId=*/env->CallIntMethod(keyEvent,
                                        gKeyEventClassInfo.getDeviceId),
        /*source=*/env->CallIntMethod(keyEvent, gKeyEventClassInfo.getSource),
        /*action=*/env->CallIntMethod(keyEvent, gKeyEventClassInfo.getAction),
        // TODO: introduce a millisecondsToNanoseconds helper:
        /*eventTime=*/
        env->CallLongMethod(keyEvent, gKeyEventClassInfo.getEventTime) *
            1000000,
        /*downTime=*/
        env->CallLongMethod(keyEvent, gKeyEventClassInfo.getDownTime) * 1000000,
        /*flags=*/env->CallIntMethod(keyEvent, gKeyEventClassInfo.getFlags),
        /*metaState=*/
        env->CallIntMethod(keyEvent, gKeyEventClassInfo.getMetaState),
        /*modifiers=*/gKeyEventClassInfo.getModifiers
            ? env->CallIntMethod(keyEvent, gKeyEventClassInfo.getModifiers)
            : 0,
        /*repeatCount=*/
        env->CallIntMethod(keyEvent, gKeyEventClassInfo.getRepeatCount),
        /*keyCode=*/
        env->CallIntMethod(keyEvent, gKeyEventClassInfo.getKeyCode),
        /*scanCode=*/
        env->CallIntMethod(keyEvent, gKeyEventClassInfo.getScanCode)
        /*unicodeChar=*/
        //env->CallIntMethod(keyEvent, gKeyEventClassInfo.getUnicodeChar)
    };
}
