/*
 * Copyright (C) 2019 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 "perfstatsd_io"

#include "io_usage.h"
#include <android-base/parseint.h>
#include <android-base/stringprintf.h>
#include <android-base/strings.h>
#include <cutils/android_filesystem_config.h>
#include <inttypes.h>
#include <pwd.h>

using namespace android::pixel::perfstatsd;
static constexpr const char *UID_IO_STATS_PATH = "/proc/uid_io/stats";
static constexpr char FMT_STR_TOTAL_USAGE[] =
    "[IO_TOTAL: %lld.%03llds] RD:%s WR:%s fsync:%" PRIu64 "\n";
static constexpr char STR_TOP_HEADER[] =
    "[IO_TOP    ]    fg bytes,    bg bytes,fgsyn,bgsyn :  UID   PKG_NAME\n";
static constexpr char FMT_STR_TOP_WRITE_USAGE[] =
    "[W%d:%6.2f%%]%12" PRIu64 ",%12" PRIu64 ",%5" PRIu64 ",%5" PRIu64 " :%6u %s\n";
static constexpr char FMT_STR_TOP_READ_USAGE[] =
    "[R%d:%6.2f%%]%12" PRIu64 ",%12" PRIu64 ",%5" PRIu64 ",%5" PRIu64 " :%6u %s\n";
static constexpr char FMT_STR_SKIP_TOP_READ[] = "(< %" PRIu64 "MB)skip RD";
static constexpr char FMT_STR_SKIP_TOP_WRITE[] = "(< %" PRIu64 "MB)skip WR";

static bool sOptDebug = false;

/* format number with comma
 * Ex: 10000 => 10,000
 */
static bool formatNum(uint64_t x, char *str, int size) {
    int len = snprintf(str, size, "%" PRIu64, x);
    if (len + 1 > size) {
        return false;
    }
    int extr = ((len - 1) / 3);
    int endpos = len + extr;
    if (endpos > size) {
        return false;
    }
    uint32_t d = 0;
    str[endpos] = 0;
    for (int i = 0, j = endpos - 1; i < len; i++) {
        d = x % 10;
        x = x / 10;
        str[j--] = '0' + d;
        if (i % 3 == 2) {
            if (j >= 0)
                str[j--] = ',';
        }
    }
    return true;
}

static bool isAppUid(uint32_t uid) {
    if (uid >= AID_APP_START) {
        return true;
    }
    return false;
}

std::vector<uint32_t> ProcPidIoStats::getNewPids() {
    std::vector<uint32_t> newpids;
    // Not exists in Previous
    for (int i = 0, len = mCurrPids.size(); i < len; i++) {
        if (std::find(mPrevPids.begin(), mPrevPids.end(), mCurrPids[i]) == mPrevPids.end()) {
            newpids.push_back(mCurrPids[i]);
        }
    }
    return newpids;
}

void ProcPidIoStats::update(bool forceAll) {
    ScopeTimer _debugTimer("update: /proc/pid/status for UID/Name mapping");
    _debugTimer.setEnabled(sOptDebug);
    if (forceAll) {
        mPrevPids.clear();
    } else {
        mPrevPids = mCurrPids;
    }
    // Get current pid list
    mCurrPids.clear();
    DIR *dir;
    struct dirent *ent;
    if ((dir = opendir("/proc/")) == NULL) {
        LOG(ERROR) << "failed on opendir '/proc/'";
        return;
    }
    while ((ent = readdir(dir)) != NULL) {
        if (ent->d_type == DT_DIR) {
            uint32_t pid;
            if (android::base::ParseUint(ent->d_name, &pid)) {
                mCurrPids.push_back(pid);
            }
        }
    }
    std::vector<uint32_t> newpids = getNewPids();
    // update mUidNameMapping only for new pids
    for (int i = 0, len = newpids.size(); i < len; i++) {
        uint32_t pid = newpids[i];
        std::string buffer;
        if (!android::base::ReadFileToString("/proc/" + std::to_string(pid) + "/status", &buffer)) {
            if (sOptDebug)
                LOG(INFO) << "/proc/" << std::to_string(pid) << "/status"
                          << ": ReadFileToString failed (process died?)";
            continue;
        }
        // --- Find Name ---
        size_t s = buffer.find("Name:");
        if (s == std::string::npos) {
            continue;
        }
        s += std::strlen("Name:");
        // find the pos of next word
        while (buffer[s] && isspace(buffer[s])) s++;
        if (buffer[s] == 0) {
            continue;
        }
        size_t e = s;
        // find the end pos of the word
        while (buffer[e] && !std::isspace(buffer[e])) e++;
        std::string pname(buffer, s, e - s);

        // --- Find Uid ---
        s = buffer.find("\nUid:", e);
        if (s == std::string::npos) {
            continue;
        }
        s += std::strlen("\nUid:");
        // find the pos of next word
        while (buffer[s] && isspace(buffer[s])) s++;
        if (buffer[s] == 0) {
            continue;
        }
        e = s;
        // find the end pos of the word
        while (buffer[e] && !std::isspace(buffer[e])) e++;
        std::string strUid(buffer, s, e - s);

        uint32_t uid = (uint32_t)std::stoi(strUid);
        mUidNameMapping[uid] = pname;
    }
}

bool ProcPidIoStats::getNameForUid(uint32_t uid, std::string *name) {
    if (mUidNameMapping.find(uid) != mUidNameMapping.end()) {
        *name = mUidNameMapping[uid];
        return true;
    }
    return false;
}

void IoStats::updateTopRead(UserIo usage) {
    UserIo tmp;
    for (int i = 0, len = IO_TOP_MAX; i < len; i++) {
        if (usage.sumRead() > mReadTop[i].sumRead()) {
            // if new read > old read, then swap values
            tmp = mReadTop[i];
            mReadTop[i] = usage;
            usage = tmp;
        }
    }
}

void IoStats::updateTopWrite(UserIo usage) {
    UserIo tmp;
    for (int i = 0, len = IO_TOP_MAX; i < len; i++) {
        if (usage.sumWrite() > mWriteTop[i].sumWrite()) {
            // if new write > old write, then swap values
            tmp = mWriteTop[i];
            mWriteTop[i] = usage;
            usage = tmp;
        }
    }
}

void IoStats::updateUnknownUidList() {
    if (!mUnknownUidList.size()) {
        return;
    }
    ScopeTimer _debugTimer("update overall UID/Name");
    _debugTimer.setEnabled(sOptDebug);
    mProcIoStats.update(false);
    for (uint32_t i = 0, len = mUnknownUidList.size(); i < len; i++) {
        uint32_t uid = mUnknownUidList[i];
        if (isAppUid(uid)) {
            // Get IO throughput for App processes
            std::string pname;
            if (!mProcIoStats.getNameForUid(uid, &pname)) {
                if (sOptDebug)
                    LOG(WARNING) << "unable to find App uid:" << uid;
                continue;
            }
            mUidNameMap[uid] = pname;
        } else {
            // Get IO throughput for system/native processes
            passwd *usrpwd = getpwuid(uid);
            if (!usrpwd) {
                if (sOptDebug)
                    LOG(WARNING) << "unable to find uid:" << uid << " by getpwuid";
                continue;
            }
            mUidNameMap[uid] = std::string(usrpwd->pw_name);
            if (std::find(mUnknownUidList.begin(), mUnknownUidList.end(), uid) !=
                mUnknownUidList.end()) {
            }
        }
        mUnknownUidList.erase(std::remove(mUnknownUidList.begin(), mUnknownUidList.end(), uid),
                              mUnknownUidList.end());
    }

    if (sOptDebug && mUnknownUidList.size() > 0) {
        std::stringstream msg;
        msg << "Some UID/Name can't be retrieved: ";
        for (const auto &i : mUnknownUidList) {
            msg << i << ", ";
        }
        LOG(WARNING) << msg.str();
    }
    mUnknownUidList.clear();
}

std::unordered_map<uint32_t, UserIo> IoStats::calcIncrement(
    const std::unordered_map<uint32_t, UserIo> &data) {
    std::unordered_map<uint32_t, UserIo> diffs;
    for (const auto &it : data) {
        const UserIo &d = it.second;
        // If data not existed, copy one, else calculate the increment.
        if (mPrevious.find(d.uid) == mPrevious.end()) {
            diffs[d.uid] = d;
        } else {
            diffs[d.uid] = d - mPrevious[d.uid];
        }
        // If uid not existed in UidNameMap, then add into unknown list
        if ((diffs[d.uid].sumRead() || diffs[d.uid].sumWrite()) &&
            mUidNameMap.find(d.uid) == mUidNameMap.end()) {
            mUnknownUidList.push_back(d.uid);
        }
    }
    // update Uid/Name mapping for dump()
    updateUnknownUidList();
    return diffs;
}

void IoStats::calcAll(std::unordered_map<uint32_t, UserIo> &&data) {
    // if mList == mNow, it's in init state.
    if (mLast == mNow) {
        mPrevious = std::move(data);
        mLast = mNow;
        mNow = std::chrono::system_clock::now();
        mProcIoStats.update(true);
        for (const auto &d : data) {
            mUnknownUidList.push_back(d.first);
        }
        updateUnknownUidList();
        return;
    }
    mLast = mNow;
    mNow = std::chrono::system_clock::now();

    // calculate incremental IO throughput
    std::unordered_map<uint32_t, UserIo> amounts = calcIncrement(data);
    // assign current data to Previous for next calculating
    mPrevious = std::move(data);
    // Reset Total and Tops
    mTotal.reset();
    for (int i = 0, len = IO_TOP_MAX; i < len; i++) {
        mReadTop[i].reset();
        mWriteTop[i].reset();
    }
    for (const auto &it : amounts) {
        const UserIo &d = it.second;
        // Add into total
        mTotal = mTotal + d;
        // Check if it's top
        updateTopRead(d);
        updateTopWrite(d);
    }
}

/* Dump IO usage (Sample Log)
 *
 * [IO_TOTAL: 10.160s] RD:371,703,808 WR:15,929,344 fsync:567
 * [TOP Usage ]    fg bytes,    bg bytes,fgsyn,bgsyn :  UID   NAME
 * [R1: 33.99%]           0,    73240576,    0,  240 : 10016 .android.gms.ui
 * [R2: 28.34%]    16039936,    45027328,    1,   21 : 10082 -
 * [R3: 16.54%]    11243520,    24395776,    0,   25 : 10055 -
 * [R4: 10.93%]    22241280,     1318912,    0,    1 : 10123 oid.apps.photos
 * [R5: 10.19%]    21528576,      421888,   23,   20 : 10061 android.vending
 * [W1: 58.19%]           0,     7655424,    0,  240 : 10016 .android.gms.ui
 * [W2: 17.03%]     1265664,      974848,   38,   45 : 10069 -
 * [W3: 11.30%]     1486848,           0,   58,    0 :  1000 system
 * [W4:  8.13%]      667648,      401408,   23,   20 : 10061 android.vending
 * [W5:  5.35%]           0,      704512,    0,   25 : 10055 -
 *
 */
bool IoStats::dump(std::stringstream *output) {
    std::stringstream &out = (*output);

    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(mNow - mLast);
    char readTotal[32];
    char writeTotal[32];
    if (!formatNum(mTotal.sumRead(), readTotal, 32)) {
        LOG(ERROR) << "formatNum buffer size is too small for read: " << mTotal.sumRead();
    }
    if (!formatNum(mTotal.sumWrite(), writeTotal, 32)) {
        LOG(ERROR) << "formatNum buffer size is too small for write: " << mTotal.sumWrite();
    }

    out << android::base::StringPrintf(FMT_STR_TOTAL_USAGE, ms.count() / 1000, ms.count() % 1000,
                                       readTotal, writeTotal, mTotal.fgFsync + mTotal.bgFsync);

    if (mTotal.sumRead() >= mMinSizeOfTotalRead || mTotal.sumWrite() >= mMinSizeOfTotalWrite) {
        out << STR_TOP_HEADER;
    }
    // Dump READ TOP
    if (mTotal.sumRead() < mMinSizeOfTotalRead) {
        out << android::base::StringPrintf(FMT_STR_SKIP_TOP_READ, mMinSizeOfTotalRead / 1000000)
            << std::endl;
    } else {
        for (int i = 0, len = IO_TOP_MAX; i < len; i++) {
            UserIo &target = mReadTop[i];
            if (target.sumRead() == 0) {
                break;
            }
            float percent = 100.0f * target.sumRead() / mTotal.sumRead();
            const char *package = mUidNameMap.find(target.uid) == mUidNameMap.end()
                                      ? "-"
                                      : mUidNameMap[target.uid].c_str();
            out << android::base::StringPrintf(FMT_STR_TOP_READ_USAGE, i + 1, percent,
                                               target.fgRead, target.bgRead, target.fgFsync,
                                               target.bgFsync, target.uid, package);
        }
    }

    // Dump WRITE TOP
    if (mTotal.sumWrite() < mMinSizeOfTotalWrite) {
        out << android::base::StringPrintf(FMT_STR_SKIP_TOP_WRITE, mMinSizeOfTotalWrite / 1000000)
            << std::endl;
    } else {
        for (int i = 0, len = IO_TOP_MAX; i < len; i++) {
            UserIo &target = mWriteTop[i];
            if (target.sumWrite() == 0) {
                break;
            }
            float percent = 100.0f * target.sumWrite() / mTotal.sumWrite();
            const char *package = mUidNameMap.find(target.uid) == mUidNameMap.end()
                                      ? "-"
                                      : mUidNameMap[target.uid].c_str();
            out << android::base::StringPrintf(FMT_STR_TOP_WRITE_USAGE, i + 1, percent,
                                               target.fgWrite, target.bgWrite, target.fgFsync,
                                               target.bgFsync, target.uid, package);
        }
    }
    return true;
}

static bool loadDataFromLine(std::string &&line, UserIo &data) {
    std::vector<std::string> fields = android::base::Split(line, " ");
    if (fields.size() < 11 || !android::base::ParseUint(fields[0], &data.uid) ||
        !android::base::ParseUint(fields[3], &data.fgRead) ||
        !android::base::ParseUint(fields[4], &data.fgWrite) ||
        !android::base::ParseUint(fields[7], &data.bgRead) ||
        !android::base::ParseUint(fields[8], &data.bgWrite) ||
        !android::base::ParseUint(fields[9], &data.fgFsync) ||
        !android::base::ParseUint(fields[10], &data.bgFsync)) {
        LOG(WARNING) << "Invalid uid I/O stats: \"" << line << "\"";
        return false;
    }
    return true;
}

void ScopeTimer::dump(std::string *outAppend) {
    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
        std::chrono::system_clock::now() - mStart);
    outAppend->append("duration (");
    outAppend->append(mName);
    outAppend->append("): ");
    outAppend->append(std::to_string(ms.count()));
    outAppend->append("ms");
}

/*
 * setOptions - IoUsage supports following options
 *     iostats.min : skip dump when R/W amount is lower than the value
 *     iostats.read.min : skip dump when READ amount is lower than the value
 *     iostats.write.min : skip dump when WRITE amount is lower than the value
 *     iostats.debug : 1 - to enable debug log; 0 - disabled
 */
void IoUsage::setOptions(const std::string &key, const std::string &value) {
    std::stringstream out;
    out << "set IO options: " << key << " , " << value;
    if (key == "iostats.min" || key == "iostats.read.min" || key == "iostats.write.min" ||
        key == "iostats.debug") {
        uint64_t val = 0;
        if (!android::base::ParseUint(value, &val)) {
            out << "!!!! unable to parse value to uint64";
            LOG(ERROR) << out.str();
            return;
        }
        if (key == "iostats.min") {
            mStats.setDumpThresholdSizeForRead(val);
            mStats.setDumpThresholdSizeForWrite(val);
        } else if (key == "iostats.disabled") {
            mDisabled = (val != 0);
        } else if (key == "iostats.read.min") {
            mStats.setDumpThresholdSizeForRead(val);
        } else if (key == "iostats.write.min") {
            mStats.setDumpThresholdSizeForWrite(val);
        } else if (key == "iostats.debug") {
            sOptDebug = (val != 0);
        }
        LOG(INFO) << out.str() << ": Success";
    }
}

void IoUsage::refresh(void) {
    if (mDisabled)
        return;
    ScopeTimer _debugTimer("refresh");
    _debugTimer.setEnabled(sOptDebug);
    std::string buffer;
    if (!android::base::ReadFileToString(UID_IO_STATS_PATH, &buffer)) {
        LOG(ERROR) << UID_IO_STATS_PATH << ": ReadFileToString failed";
    }
    if (sOptDebug)
        LOG(INFO) << "read " << UID_IO_STATS_PATH << " OK.";
    std::vector<std::string> lines = android::base::Split(std::move(buffer), "\n");
    std::unordered_map<uint32_t, UserIo> datas;
    for (uint32_t i = 0; i < lines.size(); i++) {
        if (lines[i].empty()) {
            continue;
        }
        UserIo data;
        if (!loadDataFromLine(std::move(lines[i]), data))
            continue;
        datas[data.uid] = data;
    }
    mStats.calcAll(std::move(datas));
    std::stringstream out;
    mStats.dump(&out);
    const std::string &str = out.str();
    if (sOptDebug) {
        LOG(INFO) << str;
        LOG(INFO) << "output append length:" << str.length();
    }
    append((std::string &)str);
}
