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

#include <sys/sysinfo.h>

#include <android-base/stringprintf.h>
#include <cputimeinstate.h>

namespace android {

static constexpr uint64_t NSEC_PER_MSEC = 1000000;

static struct {
    jclass clazz;
    jmethodID put;
    jmethodID get;
} gSparseArrayClassInfo;

static jfieldID gmData;

static jlongArray getUidArray(JNIEnv *env, jobject sparseAr, uint32_t uid, jsize sz) {
    jlongArray ar = (jlongArray)env->CallObjectMethod(sparseAr, gSparseArrayClassInfo.get, uid);
    if (!ar) {
        ar = env->NewLongArray(sz);
        if (ar == nullptr) return ar;
        env->CallVoidMethod(sparseAr, gSparseArrayClassInfo.put, uid, ar);
    }
    return ar;
}

static void copy2DVecToArray(JNIEnv *env, jlongArray ar, std::vector<std::vector<uint64_t>> &vec) {
    jsize start = 0;
    for (auto &subVec : vec) {
        for (uint32_t i = 0; i < subVec.size(); ++i) subVec[i] /= NSEC_PER_MSEC;
        env->SetLongArrayRegion(ar, start, subVec.size(),
                                reinterpret_cast<const jlong *>(subVec.data()));
        start += subVec.size();
    }
}

static jboolean KernelCpuUidFreqTimeBpfMapReader_removeUidRange(JNIEnv *env, jclass, jint startUid,
                                                                jint endUid) {
    for (jint uid = startUid; uid <= endUid; ++uid) {
        if (!android::bpf::clearUidTimes(uid)) return false;
    }
    return true;
}

static jboolean KernelCpuUidFreqTimeBpfMapReader_readBpfData(JNIEnv *env, jobject thiz) {
    static uint64_t lastUpdate = 0;
    uint64_t newLastUpdate = lastUpdate;
    auto sparseAr = env->GetObjectField(thiz, gmData);
    if (sparseAr == nullptr) return false;
    auto data = android::bpf::getUidsUpdatedCpuFreqTimes(&newLastUpdate);
    if (!data.has_value()) return false;

    jsize s = 0;
    for (auto &[uid, times] : *data) {
        if (s == 0) {
            for (const auto &subVec : times) s += subVec.size();
        }
        jlongArray ar = getUidArray(env, sparseAr, uid, s);
        if (ar == nullptr) return false;
        copy2DVecToArray(env, ar, times);
    }
    lastUpdate = newLastUpdate;
    return true;
}

static const JNINativeMethod gFreqTimeMethods[] = {
        {"removeUidRange", "(II)Z", (void *)KernelCpuUidFreqTimeBpfMapReader_removeUidRange},
        {"readBpfData", "()Z", (void *)KernelCpuUidFreqTimeBpfMapReader_readBpfData},
};

static jboolean KernelCpuUidActiveTimeBpfMapReader_readBpfData(JNIEnv *env, jobject thiz) {
    static uint64_t lastUpdate = 0;
    uint64_t newLastUpdate = lastUpdate;
    auto sparseAr = env->GetObjectField(thiz, gmData);
    if (sparseAr == nullptr) return false;
    auto data = android::bpf::getUidsUpdatedConcurrentTimes(&newLastUpdate);
    if (!data.has_value()) return false;

    for (auto &[uid, times] : *data) {
        // TODO: revise calling code so we can divide by NSEC_PER_MSEC here instead
        for (auto &time : times.active) time /= NSEC_PER_MSEC;
        jlongArray ar = getUidArray(env, sparseAr, uid, times.active.size());
        if (ar == nullptr) return false;
        env->SetLongArrayRegion(ar, 0, times.active.size(),
                                reinterpret_cast<const jlong *>(times.active.data()));
    }
    lastUpdate = newLastUpdate;
    return true;
}

static jlongArray KernelCpuUidActiveTimeBpfMapReader_getDataDimensions(JNIEnv *env, jobject) {
    jlong nCpus = get_nprocs_conf();

    auto ar = env->NewLongArray(1);
    if (ar != nullptr) env->SetLongArrayRegion(ar, 0, 1, &nCpus);
    return ar;
}

static const JNINativeMethod gActiveTimeMethods[] = {
        {"readBpfData", "()Z", (void *)KernelCpuUidActiveTimeBpfMapReader_readBpfData},
        {"getDataDimensions", "()[J", (void *)KernelCpuUidActiveTimeBpfMapReader_getDataDimensions},
};

static jboolean KernelCpuUidClusterTimeBpfMapReader_readBpfData(JNIEnv *env, jobject thiz) {
    static uint64_t lastUpdate = 0;
    uint64_t newLastUpdate = lastUpdate;
    auto sparseAr = env->GetObjectField(thiz, gmData);
    if (sparseAr == nullptr) return false;
    auto data = android::bpf::getUidsUpdatedConcurrentTimes(&newLastUpdate);
    if (!data.has_value()) return false;

    jsize s = 0;
    for (auto &[uid, times] : *data) {
        if (s == 0) {
            for (const auto &subVec : times.policy) s += subVec.size();
        }
        jlongArray ar = getUidArray(env, sparseAr, uid, s);
        if (ar == nullptr) return false;
        copy2DVecToArray(env, ar, times.policy);
    }
    lastUpdate = newLastUpdate;
    return true;
}

static jlongArray KernelCpuUidClusterTimeBpfMapReader_getDataDimensions(JNIEnv *env, jobject) {
    auto times = android::bpf::getUidConcurrentTimes(0);
    if (!times.has_value()) return nullptr;

    std::vector<jlong> clusterCores;
    for (const auto &vec : times->policy) clusterCores.push_back(vec.size());
    auto ar = env->NewLongArray(clusterCores.size());
    if (ar != nullptr) env->SetLongArrayRegion(ar, 0, clusterCores.size(), clusterCores.data());
    return ar;
}

static const JNINativeMethod gClusterTimeMethods[] = {
        {"readBpfData", "()Z", (void *)KernelCpuUidClusterTimeBpfMapReader_readBpfData},
        {"getDataDimensions", "()[J",
         (void *)KernelCpuUidClusterTimeBpfMapReader_getDataDimensions},
};

struct readerMethods {
    const char *name;
    const JNINativeMethod *methods;
    int numMethods;
};

static const readerMethods gAllMethods[] = {
        {"KernelCpuUidFreqTimeBpfMapReader", gFreqTimeMethods, NELEM(gFreqTimeMethods)},
        {"KernelCpuUidActiveTimeBpfMapReader", gActiveTimeMethods, NELEM(gActiveTimeMethods)},
        {"KernelCpuUidClusterTimeBpfMapReader", gClusterTimeMethods, NELEM(gClusterTimeMethods)},
};

int register_com_android_internal_os_KernelCpuUidBpfMapReader(JNIEnv *env) {
    gSparseArrayClassInfo.clazz = FindClassOrDie(env, "android/util/SparseArray");
    gSparseArrayClassInfo.clazz = MakeGlobalRefOrDie(env, gSparseArrayClassInfo.clazz);
    gSparseArrayClassInfo.put =
            GetMethodIDOrDie(env, gSparseArrayClassInfo.clazz, "put", "(ILjava/lang/Object;)V");
    gSparseArrayClassInfo.get =
            GetMethodIDOrDie(env, gSparseArrayClassInfo.clazz, "get", "(I)Ljava/lang/Object;");
    constexpr auto readerName = "com/android/internal/os/KernelCpuUidBpfMapReader";
    auto c = FindClassOrDie(env, readerName);
    gmData = GetFieldIDOrDie(env, c, "mData", "Landroid/util/SparseArray;");

    int ret = 0;
    for (const auto &m : gAllMethods) {
        auto fullName = android::base::StringPrintf("%s$%s", readerName, m.name);
        ret = RegisterMethodsOrDie(env, fullName.c_str(), m.methods, m.numMethods);
        if (ret < 0) break;
    }
    return ret;
}

} // namespace android
