/*
 * 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.
 */

#define LOG_TAG "android.hardware.tv.hdmi.cec"
#include <android-base/logging.h>
#include <fcntl.h>
#include <utils/Log.h>

#include <hardware/hardware.h>
#include <hardware/hdmi_cec.h>
#include "HdmiCecMock.h"

using ndk::ScopedAStatus;

namespace android {
namespace hardware {
namespace tv {
namespace hdmi {
namespace cec {
namespace implementation {

void HdmiCecMock::serviceDied(void* cookie) {
    ALOGE("HdmiCecMock died");
    auto hdmiCecMock = static_cast<HdmiCecMock*>(cookie);
    hdmiCecMock->closeCallback();
}

ScopedAStatus HdmiCecMock::addLogicalAddress(CecLogicalAddress addr, Result* _aidl_return) {
    // Have a list to maintain logical addresses
    mLogicalAddresses.push_back(addr);
    *_aidl_return = Result::SUCCESS;
    return ScopedAStatus::ok();
}

ScopedAStatus HdmiCecMock::clearLogicalAddress() {
    // Remove logical address from the list
    mLogicalAddresses = {};
    return ScopedAStatus::ok();
}

ScopedAStatus HdmiCecMock::enableAudioReturnChannel(int32_t portId __unused, bool enable __unused) {
    // Maintain ARC status
    return ScopedAStatus::ok();
}

ScopedAStatus HdmiCecMock::getCecVersion(int32_t* _aidl_return) {
    // Maintain a cec version and return it
    *_aidl_return = mCecVersion;
    return ScopedAStatus::ok();
}

ScopedAStatus HdmiCecMock::getPhysicalAddress(int32_t* _aidl_return) {
    // Maintain a physical address and return it
    // Default 0xFFFF, update on hotplug event
    *_aidl_return = mPhysicalAddress;
    return ScopedAStatus::ok();
}

ScopedAStatus HdmiCecMock::getVendorId(int32_t* _aidl_return) {
    *_aidl_return = mCecVendorId;
    return ScopedAStatus::ok();
}

ScopedAStatus HdmiCecMock::sendMessage(const CecMessage& message, SendMessageResult* _aidl_return) {
    if (message.body.size() == 0) {
        *_aidl_return = SendMessageResult::NACK;
    } else {
        sendMessageToFifo(message);
        *_aidl_return = SendMessageResult::SUCCESS;
    }
    return ScopedAStatus::ok();
}

ScopedAStatus HdmiCecMock::setCallback(const std::shared_ptr<IHdmiCecCallback>& callback) {
    closeCallback();

    if (callback != nullptr) {
        mCallback = callback;
        mDeathRecipient =
                ndk::ScopedAIBinder_DeathRecipient(AIBinder_DeathRecipient_new(serviceDied));
        AIBinder_linkToDeath(callback->asBinder().get(), mDeathRecipient.get(), this /* cookie */);

        mInputFile = open(CEC_MSG_IN_FIFO, O_RDWR | O_CLOEXEC);
        mOutputFile = open(CEC_MSG_OUT_FIFO, O_RDWR | O_CLOEXEC);
        pthread_create(&mThreadId, NULL, __threadLoop, this);
        pthread_setname_np(mThreadId, "hdmi_cec_loop");
    }
    return ScopedAStatus::ok();
}

ScopedAStatus HdmiCecMock::setLanguage(const std::string& language) {
    if (language.size() != 3) {
        ALOGE("[halimp_aidl] Wrong language code: expected 3 letters, but it was %zu",
              language.size());
        return ScopedAStatus::ok();
    }
    // TODO Validate if language is a valid language code
    const char* languageStr = language.c_str();
    int convertedLanguage = ((languageStr[0] & 0xFF) << 16) | ((languageStr[1] & 0xFF) << 8) |
                            (languageStr[2] & 0xFF);
    mOptionLanguage = convertedLanguage;
    return ScopedAStatus::ok();
}

ScopedAStatus HdmiCecMock::enableWakeupByOtp(bool value) {
    mOptionWakeUp = value;
    return ScopedAStatus::ok();
}

ScopedAStatus HdmiCecMock::enableCec(bool value) {
    mOptionEnableCec = value;
    return ScopedAStatus::ok();
}

ScopedAStatus HdmiCecMock::enableSystemCecControl(bool value) {
    mOptionSystemCecControl = value;
    return ScopedAStatus::ok();
}

void* HdmiCecMock::__threadLoop(void* user) {
    HdmiCecMock* const self = static_cast<HdmiCecMock*>(user);
    self->threadLoop();
    return 0;
}

int HdmiCecMock::readMessageFromFifo(unsigned char* buf, int msgCount) {
    if (msgCount <= 0 || !buf) {
        return 0;
    }

    int ret = -1;
    // Maybe blocked at driver
    ret = read(mInputFile, buf, msgCount);
    if (ret < 0) {
        ALOGE("[halimp_aidl] read :%s failed, ret:%d\n", CEC_MSG_IN_FIFO, ret);
        return -1;
    }

    return ret;
}

int HdmiCecMock::sendMessageToFifo(const CecMessage& message) {
    unsigned char msgBuf[CEC_MESSAGE_BODY_MAX_LENGTH + 1] = {0};
    int ret = -1;

    msgBuf[0] = ((static_cast<uint8_t>(message.initiator) & 0xf) << 4) |
                (static_cast<uint8_t>(message.destination) & 0xf);

    size_t length = std::min(static_cast<size_t>(message.body.size()),
                             static_cast<size_t>(CEC_MESSAGE_BODY_MAX_LENGTH));
    for (size_t i = 0; i < length; ++i) {
        msgBuf[i + 1] = static_cast<unsigned char>(message.body[i]);
    }

    // Open the output pipe for writing outgoing cec message
    mOutputFile = open(CEC_MSG_OUT_FIFO, O_WRONLY | O_CLOEXEC);
    if (mOutputFile < 0) {
        ALOGE("[halimp_aidl] file open failed for writing");
        return -1;
    }

    // Write message into the output pipe
    ret = write(mOutputFile, msgBuf, length + 1);
    close(mOutputFile);
    if (ret < 0) {
        ALOGE("[halimp_aidl] write :%s failed, ret:%d\n", CEC_MSG_OUT_FIFO, ret);
        return -1;
    }
    return ret;
}

void HdmiCecMock::printCecMsgBuf(const char* msg_buf, int len) {
    int i, size = 0;
    const int bufSize = CEC_MESSAGE_BODY_MAX_LENGTH * 3;
    // Use 2 characters for each byte in the message plus 1 space
    char buf[bufSize] = {0};

    // Messages longer than max length will be truncated.
    for (i = 0; i < len && size < bufSize; i++) {
        size += sprintf(buf + size, " %02x", msg_buf[i]);
    }
    ALOGD("[halimp_aidl] %s, msg:%.*s", __FUNCTION__, size, buf);
}

void HdmiCecMock::handleCecMessage(unsigned char* msgBuf, int msgSize) {
    CecMessage message;
    size_t length = std::min(static_cast<size_t>(msgSize - 1),
                             static_cast<size_t>(CEC_MESSAGE_BODY_MAX_LENGTH));
    message.body.resize(length);

    for (size_t i = 0; i < length; ++i) {
        message.body[i] = static_cast<uint8_t>(msgBuf[i + 1]);
        ALOGD("[halimp_aidl] msg body %x", message.body[i]);
    }

    message.initiator = static_cast<CecLogicalAddress>((msgBuf[0] >> 4) & 0xf);
    ALOGD("[halimp_aidl] msg init %hhd", message.initiator);
    message.destination = static_cast<CecLogicalAddress>((msgBuf[0] >> 0) & 0xf);
    ALOGD("[halimp_aidl] msg dest %hhd", message.destination);

    if (mCallback != nullptr) {
        mCallback->onCecMessage(message);
    }
}

void HdmiCecMock::threadLoop() {
    ALOGD("[halimp_aidl] threadLoop start.");
    unsigned char msgBuf[CEC_MESSAGE_BODY_MAX_LENGTH];
    int r = -1;

    // Open the input pipe
    while (mCecThreadRun && mInputFile < 0) {
        usleep(1000 * 1000);
        mInputFile = open(CEC_MSG_IN_FIFO, O_RDONLY | O_CLOEXEC);
    }
    ALOGD("[halimp_aidl] file open ok, fd = %d.", mInputFile);

    while (mCecThreadRun) {
        if (!mOptionSystemCecControl) {
            usleep(1000 * 1000);
            continue;
        }

        memset(msgBuf, 0, sizeof(msgBuf));
        // Try to get a message from dev.
        // echo -n -e '\x04\x83' >> /dev/cec
        r = readMessageFromFifo(msgBuf, CEC_MESSAGE_BODY_MAX_LENGTH);
        if (r <= 1) {
            // Ignore received ping messages
            continue;
        }

        printCecMsgBuf((const char*)msgBuf, r);

        if (((msgBuf[0] >> 4) & 0xf) == 0xf) {
            // The message is a hotplug event, handled by HDMI HAL.
            continue;
        }

        handleCecMessage(msgBuf, r);
    }

    ALOGD("[halimp_aidl] thread end.");
}

HdmiCecMock::HdmiCecMock() {
    ALOGD("[halimp_aidl] Opening a virtual CEC HAL for testing and virtual machine.");
    mCallback = nullptr;
    mDeathRecipient = ndk::ScopedAIBinder_DeathRecipient(nullptr);
}

void HdmiCecMock::closeCallback() {
    if (mCallback != nullptr) {
        ALOGD("[halimp_aidl] HdmiCecMock close the current callback.");
        mCallback = nullptr;
        mDeathRecipient = ndk::ScopedAIBinder_DeathRecipient(nullptr);
        mCecThreadRun = false;
        pthread_join(mThreadId, NULL);
    }
}

HdmiCecMock::~HdmiCecMock() {
    ALOGD("[halimp_aidl] HdmiCecMock shutting down.");
    closeCallback();
}

}  // namespace implementation
}  // namespace cec
}  // namespace hdmi
}  // namespace tv
}  // namespace hardware
}  // namespace android
