/*
 * Copyright (C) 2016 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 "NanohubHAL"

#include <cassert>
#include <cerrno>
#include <cinttypes>

#include <endian.h>

#include <vector>

#include <utils/Log.h>

#include <endian.h>

#include <hardware/context_hub.h>
#include "nanohub_perdevice.h"
#include "system_comms.h"
#include "nanohubhal.h"

namespace android {

namespace nanohub {

static void readAppName(MessageBuf &buf, hub_app_name_t &name)
{
    name.id = buf.readU64();
}

static void writeAppName(MessageBuf &buf, const hub_app_name_t &name)
{
    buf.writeU64(name.id);
}

static void readNanohubAppInfo(MessageBuf &buf, NanohubAppInfo &info)
{
    size_t pos = buf.getPos();
    readAppName(buf, info.name);
    info.version = buf.readU32();
    info.flashUse = buf.readU32();
    info.ramUse = buf.readU32();
    if ((buf.getPos() - pos) != sizeof(info)) {
        ALOGE("%s: failed to read object", __func__);
    }
}

static void readNanohubMemInfo(MessageBuf &buf,  NanohubMemInfo &mi)
{
    size_t pos = buf.getPos();
    mi.flashSz = buf.readU32();
    mi.blSz = buf.readU32();
    mi.osSz = buf.readU32();
    mi.sharedSz = buf.readU32();
    mi.eeSz = buf.readU32();
    mi.ramSz = buf.readU32();

    mi.blUse = buf.readU32();
    mi.osUse = buf.readU32();
    mi.sharedUse = buf.readU32();
    mi.eeUse = buf.readU32();
    mi.ramUse = buf.readU32();
    if ((buf.getPos() - pos) != sizeof(mi)) {
        ALOGE("%s: failed to read object", __func__);
    }
}

NanohubRsp::NanohubRsp(MessageBuf &buf, bool no_status)
{
    // all responses start with command
    // most of them have 4-byte status (result code)
    buf.reset();
    cmd = buf.readU8();
    if (!buf.getSize()) {
        status = -EINVAL;
    } else if (no_status) {
        status = 0;
    } else {
        status = buf.readU32();
    }
}

int SystemComm::sendToSystem(const void *data, size_t len)
{
    if (NanoHub::messageTracingEnabled()) {
        dumpBuffer("HAL -> SYS", getSystem()->mHostIfAppName, 0, data, len);
    }
    return NanoHub::sendToDevice(&getSystem()->mHostIfAppName, data, len);
}

int SystemComm::AppInfoSession::setup(const hub_message_t *)
{
    std::lock_guard<std::mutex> _l(mLock);
    int suggestedSize = mAppInfo.size() ? mAppInfo.size() : 20;

    mAppInfo.clear();
    mAppInfo.reserve(suggestedSize);
    setState(SESSION_USER);

    return requestNext();
}

inline hub_app_name_t deviceAppNameToHost(const hub_app_name_t src)
{
    hub_app_name_t res = { .id = le64toh(src.id) };
    return res;
}

inline hub_app_name_t hostAppNameToDevice(const hub_app_name_t src)
{
    hub_app_name_t res = { .id = htole64(src.id) };
    return res;
}

int SystemComm::AppInfoSession::handleRx(MessageBuf &buf)
{
    std::lock_guard<std::mutex> _l(mLock);

    NanohubRsp rsp(buf, true);
    if (rsp.cmd != NANOHUB_QUERY_APPS) {
        return 1;
    }
    size_t len = buf.getRoom();
    if (len != sizeof(NanohubAppInfo) && len) {
        ALOGE("%s: Invalid data size; have %zu, need %zu", __func__,
              len, sizeof(NanohubAppInfo));
        return -EINVAL;
    }
    if (getState() != SESSION_USER) {
        ALOGE("%s: Invalid state; have %d, need %d", __func__, getState(), SESSION_USER);
        return -EINVAL;
    }
    if (len) {
        NanohubAppInfo info;
        readNanohubAppInfo(buf, info);
        hub_app_info appInfo;
        appInfo.num_mem_ranges = 0;
        if (info.flashUse != NANOHUB_MEM_SZ_UNKNOWN) {
            mem_range_t &range = appInfo.mem_usage[appInfo.num_mem_ranges++];
            range.type = HUB_MEM_TYPE_MAIN;
            range.total_bytes = info.flashUse;
        }
        if (info.ramUse != NANOHUB_MEM_SZ_UNKNOWN) {
            mem_range_t &range = appInfo.mem_usage[appInfo.num_mem_ranges++];
            range.type = HUB_MEM_TYPE_RAM;
            range.total_bytes = info.ramUse;
        }

        appInfo.app_name = info.name;
        appInfo.version = info.version;

        mAppInfo.push_back(appInfo);
        return requestNext();
    } else {
        sendToApp(CONTEXT_HUB_QUERY_APPS,
                        static_cast<const void *>(mAppInfo.data()),
                        mAppInfo.size() * sizeof(mAppInfo[0]));
        complete();
    }

    return 0;
}

int SystemComm::AppInfoSession::requestNext()
{
    char data[MAX_RX_PACKET];
    MessageBuf buf(data, sizeof(data));
    buf.writeU8(NANOHUB_QUERY_APPS);
    buf.writeU32(mAppInfo.size());
    return sendToSystem(buf.getData(), buf.getPos());
}

int SystemComm::MemInfoSession::setup(const hub_message_t *)
{
    std::lock_guard<std::mutex> _l(mLock);
    char data[MAX_RX_PACKET];
    MessageBuf buf(data, sizeof(data));
    buf.writeU8(NANOHUB_QUERY_MEMINFO);

    setState(SESSION_USER);
    return sendToSystem(buf.getData(), buf.getPos());
}

int SystemComm::MemInfoSession::handleRx(MessageBuf &buf)
{
    std::lock_guard<std::mutex> _l(mLock);
    NanohubRsp rsp(buf, true);

    if (rsp.cmd != NANOHUB_QUERY_MEMINFO)
        return 1;

    size_t len = buf.getRoom();

    if (len != sizeof(NanohubMemInfo)) {
        ALOGE("%s: Invalid data size: %zu", __func__, len);
        return -EINVAL;
    }
    if (getState() != SESSION_USER) {
        ALOGE("%s: Invalid state; have %d, need %d", __func__, getState(), SESSION_USER);
        return -EINVAL;
    }

    NanohubMemInfo mi;
    readNanohubMemInfo(buf, mi);
    std::vector<mem_range_t> ranges;
    ranges.reserve(4);

    //if each is valid, copy to output area
    if (mi.sharedSz != NANOHUB_MEM_SZ_UNKNOWN &&
        mi.sharedUse != NANOHUB_MEM_SZ_UNKNOWN)
        ranges.push_back({
            .type = HUB_MEM_TYPE_MAIN,
            .total_bytes = mi.sharedSz,
            .free_bytes = mi.sharedSz - mi.sharedUse,
        });

    if (mi.osSz != NANOHUB_MEM_SZ_UNKNOWN &&
        mi.osUse != NANOHUB_MEM_SZ_UNKNOWN)
        ranges.push_back({
            .type = HUB_MEM_TYPE_OS,
            .total_bytes = mi.osSz,
            .free_bytes = mi.osSz - mi.osUse,
        });

    if (mi.eeSz != NANOHUB_MEM_SZ_UNKNOWN &&
        mi.eeUse != NANOHUB_MEM_SZ_UNKNOWN)
        ranges.push_back({
            .type = HUB_MEM_TYPE_EEDATA,
            .total_bytes = mi.eeSz,
            .free_bytes = mi.eeSz - mi.eeUse,
        });

    if (mi.ramSz != NANOHUB_MEM_SZ_UNKNOWN &&
        mi.ramUse != NANOHUB_MEM_SZ_UNKNOWN)
        ranges.push_back({
            .type = HUB_MEM_TYPE_RAM,
            .total_bytes = mi.ramSz,
            .free_bytes = mi.ramSz - mi.ramUse,
        });

    //send it out
    sendToApp(CONTEXT_HUB_QUERY_MEMORY,
              static_cast<const void *>(ranges.data()),
              ranges.size() * sizeof(ranges[0]));

    complete();

    return 0;
}

int SystemComm::AppMgmtSession::setup(const hub_message_t *appMsg)
{
    std::lock_guard<std::mutex> _l(mLock);

    char data[MAX_RX_PACKET];
    MessageBuf buf(data, sizeof(data));
    const uint8_t *msgData = static_cast<const uint8_t*>(appMsg->message);

    mCmd = appMsg->message_type;
    mLen = appMsg->message_len;
    mPos = 0;

    switch (mCmd) {
    case  CONTEXT_HUB_APPS_ENABLE:
        return setupMgmt(appMsg, NANOHUB_EXT_APPS_ON);
    case  CONTEXT_HUB_APPS_DISABLE:
        return setupMgmt(appMsg, NANOHUB_EXT_APPS_OFF);
    case  CONTEXT_HUB_UNLOAD_APP:
        return setupMgmt(appMsg, NANOHUB_EXT_APP_DELETE);
    case  CONTEXT_HUB_LOAD_APP:
    {
        mData.clear();
        mData = std::vector<uint8_t>(msgData, msgData + mLen);
        const load_app_request_t *appReq = static_cast<const load_app_request_t*>(appMsg->message);
        if (appReq == nullptr || mLen <= sizeof(*appReq)) {
            ALOGE("%s: Invalid app header: too short\n", __func__);
            return -EINVAL;
        }
        mAppName = appReq->app_binary.app_id;
        setState(TRANSFER);

        buf.writeU8(NANOHUB_START_UPLOAD);
        buf.writeU8(0);
        buf.writeU32(mLen);
        return sendToSystem(buf.getData(), buf.getPos());
    }

    case  CONTEXT_HUB_OS_REBOOT:
        setState(REBOOT);
        buf.writeU8(NANOHUB_REBOOT);
        return sendToSystem(buf.getData(), buf.getPos());
    }

    return -EINVAL;
}

int SystemComm::AppMgmtSession::setupMgmt(const hub_message_t *appMsg, uint32_t cmd)
{
    const hub_app_name_t &appName = *static_cast<const hub_app_name_t*>(appMsg->message);
    if (appMsg->message_len != sizeof(appName)) {
        return -EINVAL;
    }

    char data[MAX_RX_PACKET];
    MessageBuf buf(data, sizeof(data));
    buf.writeU8(cmd);
    writeAppName(buf, appName);
    setState(MGMT);

    return sendToSystem(buf.getData(), buf.getPos());
}

int SystemComm::AppMgmtSession::handleRx(MessageBuf &buf)
{
    int ret = 0;
    std::lock_guard<std::mutex> _l(mLock);
    NanohubRsp rsp(buf);

    switch (getState()) {
    case TRANSFER:
        ret = handleTransfer(rsp);
        break;
    case FINISH:
        ret = handleFinish(rsp);
        break;
    case RUN:
        ret = handleRun(rsp);
        break;
    case RUN_FAILED:
        ret = handleRunFailed(rsp);
        break;
    case REBOOT:
        ret = handleReboot(rsp);
        break;
    case MGMT:
        ret = handleMgmt(rsp);
        break;
    }

    return ret;
}

int SystemComm::AppMgmtSession::handleTransfer(NanohubRsp &rsp)
{
    if (rsp.cmd != NANOHUB_CONT_UPLOAD && rsp.cmd != NANOHUB_START_UPLOAD)
        return 1;

    char data[MAX_RX_PACKET];
    MessageBuf buf(data, sizeof(data));

    static_assert(NANOHUB_UPLOAD_CHUNK_SZ_MAX <= (MAX_RX_PACKET-5),
                  "Invalid chunk size");

    if (mPos < mLen) {
        uint32_t chunkSize = mLen - mPos;

        if (chunkSize > NANOHUB_UPLOAD_CHUNK_SZ_MAX) {
            chunkSize = NANOHUB_UPLOAD_CHUNK_SZ_MAX;
        }

        buf.writeU8(NANOHUB_CONT_UPLOAD);
        buf.writeU32(mPos);
        buf.writeRaw(&mData[mPos], chunkSize);
        mPos += chunkSize;
    } else {
        buf.writeU8(NANOHUB_FINISH_UPLOAD);
        setState(FINISH);
    }

    return sendToSystem(buf.getData(), buf.getPos());
}

int SystemComm::AppMgmtSession::handleFinish(NanohubRsp &rsp)
{
    if (rsp.cmd != NANOHUB_FINISH_UPLOAD)
        return 1;

    int ret = 0;
    const bool success = rsp.status != 0;
    mData.clear();

    if (success) {
        char data[MAX_RX_PACKET];
        MessageBuf buf(data, sizeof(data));
        buf.writeU8(NANOHUB_EXT_APPS_ON);
        writeAppName(buf, mAppName);
        setState(RUN);
        ret = sendToSystem(buf.getData(), buf.getPos());
    } else {
        int32_t result = NANOHUB_APP_NOT_LOADED;

        sendToApp(mCmd, &result, sizeof(result));
        complete();
    }

    return ret;
}

int SystemComm::AppMgmtSession::handleRun(NanohubRsp &rsp)
{
    if (rsp.cmd != NANOHUB_EXT_APPS_ON)
        return 1;

    MgmtStatus sts = { .value = (uint32_t)rsp.status };

    // op counter returns number of nanoapps that were started as result of the command
    // for successful start command it must be > 0
    int32_t result = sts.value > 0 && sts.op > 0 && sts.op <= 0x7F ? 0 : -1;

    ALOGI("Nanohub NEW APP START: %08" PRIX32 "\n", rsp.status);
    if (result != 0) {
        // if nanoapp failed to start we have to unload it
        char data[MAX_RX_PACKET];
        MessageBuf buf(data, sizeof(data));
        buf.writeU8(NANOHUB_EXT_APP_DELETE);
        writeAppName(buf, mAppName);
        if (sendToSystem(buf.getData(), buf.getPos()) == 0) {
            setState(RUN_FAILED);
            return 0;
        }
        ALOGE("%s: failed to send DELETE for failed app\n", __func__);
    }

    // it is either success, and we report it, or
    // it is a failure to load, and also failure to send erase command
    sendToApp(mCmd, &result, sizeof(result));
    complete();
    return 0;
}

int SystemComm::AppMgmtSession::handleRunFailed(NanohubRsp &rsp)
{
    if (rsp.cmd != NANOHUB_EXT_APP_DELETE)
        return 1;

    int32_t result = -1;

    ALOGI("%s: APP DELETE [because it failed]: %08" PRIX32 "\n", __func__, rsp.status);

    sendToApp(mCmd, &result, sizeof(result));
    complete();

    return 0;
}

/* reboot notification, when triggered by App request */
int SystemComm::AppMgmtSession::handleReboot(NanohubRsp &rsp)
{
    if (rsp.cmd != NANOHUB_REBOOT)
        return 1;
    ALOGI("Nanohub reboot status [USER REQ]: %08" PRIX32 "\n", rsp.status);

    // reboot notification is sent by SessionManager
    complete();

    return 0;
}

int SystemComm::AppMgmtSession::handleMgmt(NanohubRsp &rsp)
{
    bool valid = false;

    int32_t result = rsp.status;

    // TODO: remove this when context hub service can handle non-zero success status
    if (result > 0) {
        // something happened; assume it worked
        result = 0;
    } else if (result == 0) {
        // nothing happened; this is provably an error
        result = -1;
    }

    switch (rsp.cmd) {
    case NANOHUB_EXT_APPS_OFF:
        valid = mCmd == CONTEXT_HUB_APPS_DISABLE;
        break;
    case NANOHUB_EXT_APPS_ON:
        valid = mCmd == CONTEXT_HUB_APPS_ENABLE;
        break;
    case NANOHUB_EXT_APP_DELETE:
        valid = mCmd == CONTEXT_HUB_UNLOAD_APP;
        break;
    default:
        return 1;
    }

    ALOGI("Nanohub MGMT response: CMD=%02X; STATUS=%08" PRIX32, rsp.cmd, rsp.status);
    if (!valid) {
        ALOGE("Invalid response for this state: APP CMD=%02X", mCmd);
        return -EINVAL;
    }

    sendToApp(mCmd, &result, sizeof(result));
    complete();

    return 0;
}

int SystemComm::KeyInfoSession::setup(const hub_message_t *) {
    std::lock_guard<std::mutex> _l(mLock);
    mRsaKeyData.clear();
    setState(SESSION_USER);
    mStatus = -EBUSY;
    return requestRsaKeys();
}

int SystemComm::KeyInfoSession::handleRx(MessageBuf &buf)
{
    std::lock_guard<std::mutex> _l(mLock);
    NanohubRsp rsp(buf, true);

    if (getState() != SESSION_USER) {
        // invalid state
        mStatus = -EFAULT;
        return mStatus;
    }

    if (buf.getRoom()) {
        mRsaKeyData.insert(mRsaKeyData.end(),
                           buf.getData() + buf.getPos(),
                           buf.getData() + buf.getSize());
        return requestRsaKeys();
    } else {
        mStatus = 0;
        complete();
        return 0;
    }
}

int SystemComm::KeyInfoSession::requestRsaKeys(void)
{
    char data[MAX_RX_PACKET];
    MessageBuf buf(data, sizeof(data));

    buf.writeU8(NANOHUB_QUERY_APPS);
    buf.writeU32(mRsaKeyData.size());

    return sendToSystem(buf.getData(), buf.getPos());
}

int SystemComm::doHandleRx(const nano_message *msg)
{
    //we only care for messages from HostIF
    if (msg->hdr.appId != mHostIfAppName.id)
        return 1;

    //they must all be at least 1 byte long
    if (!msg->hdr.len) {
        return -EINVAL;
    }
    MessageBuf buf(reinterpret_cast<const char*>(msg->data), msg->hdr.len);
    if (NanoHub::messageTracingEnabled()) {
        dumpBuffer("SYS -> HAL", mHostIfAppName, 0, buf.getData(), buf.getSize());
    }
    int status = mSessions.handleRx(buf);
    if (status) {
        // provide default handler for any system message, that is not properly handled
        dumpBuffer(status > 0 ? "HAL (not handled)" : "HAL (error)",
                   mHostIfAppName, 0, buf.getData(), buf.getSize(), status);
        status = status > 0 ? 0 : status;
    }

    return status;
}

int SystemComm::SessionManager::handleRx(MessageBuf &buf)
{
    int status = 1;
    std::unique_lock<std::mutex> lk(lock);

    // pass message to all active sessions, in arbitrary order
    // 1st session that handles the message terminates the loop
    for (auto pos = sessions_.begin(); pos != sessions_.end() && status > 0; next(pos)) {
        if (!isActive(pos)) {
            continue;
        }
        Session *session = pos->second;
        status = session->handleRx(buf);
        if (status < 0) {
            session->complete();
        }
    }

    NanohubRsp rsp(buf);
    if (rsp.cmd == NANOHUB_REBOOT) {
        // if this is reboot notification, kill all sessions
        for (auto pos = sessions_.begin(); pos != sessions_.end(); next(pos)) {
            if (!isActive(pos)) {
                continue;
            }
            Session *session = pos->second;
            session->abort(-EINTR);
        }
        lk.unlock();
        // log the reboot event, if not handled
        if (status > 0) {
            ALOGW("Nanohub reboot status [UNSOLICITED]: %08" PRIX32, rsp.status);
            status = 0;
        }
        // report to java apps
        sendToApp(CONTEXT_HUB_OS_REBOOT, &rsp.status, sizeof(rsp.status));
    }

    return status;
}

int SystemComm::SessionManager::setup_and_add(int id, Session *session, const hub_message_t *appMsg)
{
    std::lock_guard<std::mutex> _l(lock);

    // scan sessions to release those that are already done
    for (auto pos = sessions_.begin(); pos != sessions_.end(); next(pos)) {
        continue;
    }

    if (sessions_.count(id) == 0 && !session->isRunning()) {
        sessions_[id] = session;
        int ret = session->setup(appMsg);
        if (ret < 0) {
            session->complete();
        }
        return ret;
    }
    return -EBUSY;
}

int SystemComm::doHandleTx(const hub_message_t *appMsg)
{
    int status = 0;

    switch (appMsg->message_type) {
    case CONTEXT_HUB_LOAD_APP:
        if (!mKeySession.haveKeys()) {
            status = mSessions.setup_and_add(CONTEXT_HUB_LOAD_APP, &mKeySession, appMsg);
            if (status < 0) {
                break;
            }
            mKeySession.wait();
            status = mKeySession.getStatus();
            if (status < 0) {
                break;
            }
        }
        status = mSessions.setup_and_add(CONTEXT_HUB_LOAD_APP, &mAppMgmtSession, appMsg);
        break;
    case CONTEXT_HUB_APPS_ENABLE:
    case CONTEXT_HUB_APPS_DISABLE:
    case CONTEXT_HUB_UNLOAD_APP:
        // all APP-modifying commands share session key, to ensure they can't happen at the same time
        status = mSessions.setup_and_add(CONTEXT_HUB_LOAD_APP, &mAppMgmtSession, appMsg);
        break;

    case CONTEXT_HUB_QUERY_APPS:
        status = mSessions.setup_and_add(CONTEXT_HUB_QUERY_APPS, &mAppInfoSession, appMsg);
        break;

    case CONTEXT_HUB_QUERY_MEMORY:
        status = mSessions.setup_and_add(CONTEXT_HUB_QUERY_MEMORY, &mMemInfoSession, appMsg);
        break;

    default:
        ALOGW("Unknown os message type %u\n", appMsg->message_type);
        return -EINVAL;
    }

   return status;
}

}; // namespace nanohub

}; // namespace android