/*
 * 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 "HeicEncoderInfoManager"
//#define LOG_NDEBUG 0

#include <cstdint>
#include <regex>

#include <cutils/properties.h>
#include <log/log_main.h>
#include <system/graphics.h>

#include <media/stagefright/MediaCodecList.h>
#include <media/stagefright/foundation/MediaDefs.h>
#include <media/stagefright/foundation/ABuffer.h>

#include "HeicEncoderInfoManager.h"

namespace android {
namespace camera3 {

HeicEncoderInfoManager::HeicEncoderInfoManager() :
        mIsInited(false),
        mMinSizeHeic(0, 0),
        mMaxSizeHeic(INT32_MAX, INT32_MAX),
        mHasHEVC(false),
        mHasHEIC(false),
        mDisableGrid(false) {
    if (initialize() == OK) {
        mIsInited = true;
    }
}

HeicEncoderInfoManager::~HeicEncoderInfoManager() {
}

bool HeicEncoderInfoManager::isSizeSupported(int32_t width, int32_t height, bool* useHeic,
        bool* useGrid, int64_t* stall, AString* hevcName) const {
    if (useHeic == nullptr || useGrid == nullptr) {
        ALOGE("%s: invalid parameters: useHeic %p, useGrid %p",
                __FUNCTION__, useHeic, useGrid);
        return false;
    }
    if (!mIsInited) return false;

    bool chooseHeic = false, enableGrid = true;
    if (mHasHEIC && width >= mMinSizeHeic.first &&
            height >= mMinSizeHeic.second && width <= mMaxSizeHeic.first &&
            height <= mMaxSizeHeic.second) {
        chooseHeic = true;
        enableGrid = false;
    } else if (mHasHEVC) {
        bool fullSizeSupportedByHevc = (width >= mMinSizeHevc.first &&
                height >= mMinSizeHevc.second &&
                width <= mMaxSizeHevc.first &&
                height <= mMaxSizeHevc.second);
        if (fullSizeSupportedByHevc && (mDisableGrid ||
                (width <= 1920 && height <= 1080))) {
            enableGrid = false;
        }
        if (hevcName != nullptr) {
            *hevcName = mHevcName;
        }
    } else {
        // No encoder available for the requested size.
        return false;
    }

    if (stall != nullptr) {
        // Find preferred encoder which advertise
        // "measured-frame-rate-WIDTHxHEIGHT-range" key.
        const FrameRateMaps& maps =
                (chooseHeic && mHeicFrameRateMaps.size() > 0) ?
                mHeicFrameRateMaps : mHevcFrameRateMaps;
        const auto& closestSize = findClosestSize(maps, width, height);
        if (closestSize == maps.end()) {
            // The "measured-frame-rate-WIDTHxHEIGHT-range" key is optional.
            // Hardcode to some default value (3.33ms * tile count) based on resolution.
            *stall = 3333333LL * width * height / (kGridWidth * kGridHeight);
            *useHeic = chooseHeic;
            *useGrid = enableGrid;
            return true;
        }

        // Derive stall durations based on average fps of the closest size.
        constexpr int64_t NSEC_PER_SEC = 1000000000LL;
        int32_t avgFps = (closestSize->second.first + closestSize->second.second)/2;
        float ratio = 1.0f * width * height /
                (closestSize->first.first * closestSize->first.second);
        *stall = ratio * NSEC_PER_SEC / avgFps;
    }

    *useHeic = chooseHeic;
    *useGrid = enableGrid;
    return true;
}

status_t HeicEncoderInfoManager::initialize() {
    mDisableGrid = property_get_bool("camera.heic.disable_grid", false);
    sp<IMediaCodecList> codecsList = MediaCodecList::getInstance();
    if (codecsList == nullptr) {
        // No media codec available.
        return OK;
    }

    sp<AMessage> heicDetails = getCodecDetails(codecsList, MEDIA_MIMETYPE_IMAGE_ANDROID_HEIC);

    if (!getHevcCodecDetails(codecsList, MEDIA_MIMETYPE_VIDEO_HEVC)) {
        if (heicDetails != nullptr) {
            ALOGE("%s: Device must support HEVC codec if HEIC codec is available!",
                    __FUNCTION__);
            return BAD_VALUE;
        }
        return OK;
    }
    mHasHEVC = true;

    // HEIC size range
    if (heicDetails != nullptr) {
        auto res = getCodecSizeRange(MEDIA_MIMETYPE_IMAGE_ANDROID_HEIC,
                heicDetails, &mMinSizeHeic, &mMaxSizeHeic, &mHeicFrameRateMaps);
        if (res != OK) {
            ALOGE("%s: Failed to get HEIC codec size range: %s (%d)", __FUNCTION__,
                    strerror(-res), res);
            return BAD_VALUE;
        }
        mHasHEIC = true;
    }

    return OK;
}

status_t HeicEncoderInfoManager::getFrameRateMaps(sp<AMessage> details, FrameRateMaps* maps) {
    if (details == nullptr || maps == nullptr) {
        ALOGE("%s: Invalid input: details: %p, maps: %p", __FUNCTION__, details.get(), maps);
        return BAD_VALUE;
    }

    for (size_t i = 0; i < details->countEntries(); i++) {
        AMessage::Type type;
        const char* entryName = details->getEntryNameAt(i, &type);
        if (type != AMessage::kTypeString) continue;
        std::regex frameRateNamePattern("measured-frame-rate-([0-9]+)[*x]([0-9]+)-range",
                std::regex_constants::icase);
        std::cmatch sizeMatch;
        if (std::regex_match(entryName, sizeMatch, frameRateNamePattern) &&
                sizeMatch.size() == 3) {
            AMessage::ItemData item = details->getEntryAt(i);
            AString fpsRangeStr;
            if (item.find(&fpsRangeStr)) {
                ALOGV("%s: %s", entryName, fpsRangeStr.c_str());
                std::regex frameRatePattern("([0-9]+)-([0-9]+)");
                std::cmatch fpsMatch;
                if (std::regex_match(fpsRangeStr.c_str(), fpsMatch, frameRatePattern) &&
                        fpsMatch.size() == 3) {
                    maps->emplace(
                            std::make_pair(stoi(sizeMatch[1]), stoi(sizeMatch[2])),
                            std::make_pair(stoi(fpsMatch[1]), stoi(fpsMatch[2])));
                } else {
                    return BAD_VALUE;
                }
            }
        }
    }
    return OK;
}

status_t HeicEncoderInfoManager::getCodecSizeRange(
        const char* codecName,
        sp<AMessage> details,
        std::pair<int32_t, int32_t>* minSize,
        std::pair<int32_t, int32_t>* maxSize,
        FrameRateMaps* frameRateMaps) {
    if (codecName == nullptr || minSize == nullptr || maxSize == nullptr ||
            details == nullptr || frameRateMaps == nullptr) {
        return BAD_VALUE;
    }

    AString sizeRange;
    auto hasItem = details->findString("size-range", &sizeRange);
    if (!hasItem) {
        ALOGE("%s: Failed to query size range for codec %s", __FUNCTION__, codecName);
        return BAD_VALUE;
    }
    ALOGV("%s: %s codec's size range is %s", __FUNCTION__, codecName, sizeRange.c_str());
    std::regex pattern("([0-9]+)[*x]([0-9]+)-([0-9]+)[*x]([0-9]+)");
    std::cmatch match;
    if (std::regex_match(sizeRange.c_str(), match, pattern)) {
        if (match.size() == 5) {
            minSize->first = stoi(match[1]);
            minSize->second = stoi(match[2]);
            maxSize->first = stoi(match[3]);
            maxSize->second = stoi(match[4]);
            if (minSize->first > maxSize->first ||
                    minSize->second > maxSize->second) {
                ALOGE("%s: Invalid %s code size range: %s",
                        __FUNCTION__, codecName, sizeRange.c_str());
                return BAD_VALUE;
            }
        } else {
            return BAD_VALUE;
        }
    }

    auto res = getFrameRateMaps(details, frameRateMaps);
    if (res != OK) {
        return res;
    }

    return OK;
}

HeicEncoderInfoManager::FrameRateMaps::const_iterator HeicEncoderInfoManager::findClosestSize(
        const FrameRateMaps& maps, int32_t width, int32_t height) const {
    int32_t minDiff = INT32_MAX;
    FrameRateMaps::const_iterator closestIter = maps.begin();
    for (auto iter = maps.begin(); iter != maps.end(); iter++) {
        // Use area difference between the sizes to approximate size
        // difference.
        int32_t diff = abs(iter->first.first * iter->first.second - width * height);
        if (diff < minDiff) {
            closestIter = iter;
            minDiff = diff;
        }
    }
    return closestIter;
}

sp<AMessage> HeicEncoderInfoManager::getCodecDetails(
        sp<IMediaCodecList> codecsList, const char* name) {
    ssize_t idx = codecsList->findCodecByType(name, true /*encoder*/);
    if (idx < 0) {
        return nullptr;
    }

    const sp<MediaCodecInfo> info = codecsList->getCodecInfo(idx);
    if (info == nullptr) {
        ALOGE("%s: Failed to get codec info for %s", __FUNCTION__, name);
        return nullptr;
    }
    const sp<MediaCodecInfo::Capabilities> caps =
            info->getCapabilitiesFor(name);
    if (caps == nullptr) {
        ALOGE("%s: Failed to get capabilities for codec %s", __FUNCTION__, name);
        return nullptr;
    }
    const sp<AMessage> details = caps->getDetails();
    if (details == nullptr) {
        ALOGE("%s: Failed to get details for codec %s", __FUNCTION__, name);
        return nullptr;
    }

    return details;
}

bool HeicEncoderInfoManager::getHevcCodecDetails(
        sp<IMediaCodecList> codecsList, const char* mime) {
    bool found = false;
    ssize_t idx = 0;
    while ((idx = codecsList->findCodecByType(mime, true /*encoder*/, idx)) >= 0) {
        const sp<MediaCodecInfo> info = codecsList->getCodecInfo(idx++);
        if (info == nullptr) {
            ALOGE("%s: Failed to get codec info for %s", __FUNCTION__, mime);
            break;
        }
        ALOGV("%s: [%s] codec found", __FUNCTION__,
                info->getCodecName());

        // Filter out software ones as they may be too slow
        if (!(info->getAttributes() & MediaCodecInfo::kFlagIsHardwareAccelerated)) {
            ALOGV("%s: [%s] Filter out software ones as they may be too slow", __FUNCTION__,
                    info->getCodecName());
            continue;
        }

        const sp<MediaCodecInfo::Capabilities> caps =
                info->getCapabilitiesFor(mime);
        if (caps == nullptr) {
            ALOGE("%s: [%s] Failed to get capabilities", __FUNCTION__,
                    info->getCodecName());
            break;
        }
        const sp<AMessage> details = caps->getDetails();
        if (details == nullptr) {
            ALOGE("%s: [%s] Failed to get details", __FUNCTION__,
                    info->getCodecName());
            break;
        }

        // Check CQ mode
        AString bitrateModes;
        auto hasItem = details->findString("feature-bitrate-modes", &bitrateModes);
        if (!hasItem) {
            ALOGE("%s: [%s] Failed to query bitrate modes", __FUNCTION__,
                    info->getCodecName());
            break;
        }
        ALOGV("%s: [%s] feature-bitrate-modes value is %d, %s",
                __FUNCTION__, info->getCodecName(), hasItem, bitrateModes.c_str());
        std::regex pattern("(^|,)CQ($|,)", std::regex_constants::icase);
        if (!std::regex_search(bitrateModes.c_str(), pattern)) {
            continue; // move on to next encoder
        }

        std::pair<int32_t, int32_t> minSizeHevc, maxSizeHevc;
        FrameRateMaps hevcFrameRateMaps;
        auto res = getCodecSizeRange(MEDIA_MIMETYPE_VIDEO_HEVC,
                details, &minSizeHevc, &maxSizeHevc, &hevcFrameRateMaps);
        if (res != OK) {
            ALOGE("%s: [%s] Failed to get size range: %s (%d)", __FUNCTION__,
                    info->getCodecName(), strerror(-res), res);
            break;
        }
        if (kGridWidth < minSizeHevc.first
                || kGridWidth > maxSizeHevc.first
                || kGridHeight < minSizeHevc.second
                || kGridHeight > maxSizeHevc.second) {
            continue; // move on to next encoder
        }

        // Found: save name, size, frame rate
        mHevcName = info->getCodecName();
        mMinSizeHevc = minSizeHevc;
        mMaxSizeHevc = maxSizeHevc;
        mHevcFrameRateMaps = hevcFrameRateMaps;

        found = true;
        break;
    }

    return found;
}

} //namespace camera3
} // namespace android
