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

#include <oboe/AudioStreamBuilder.h>
#include <oboe/Oboe.h>
#include <oboe/Utilities.h>

#include "OboeDebug.h"
#include "QuirksManager.h"

using namespace oboe;

int32_t QuirksManager::DeviceQuirks::clipBufferSize(AudioStream &stream,
            int32_t requestedSize) {
    if (!OboeGlobals::areWorkaroundsEnabled()) {
        return requestedSize;
    }
    int bottomMargin = kDefaultBottomMarginInBursts;
    int topMargin = kDefaultTopMarginInBursts;
    if (isMMapUsed(stream)) {
        if (stream.getSharingMode() == SharingMode::Exclusive) {
            bottomMargin = getExclusiveBottomMarginInBursts();
            topMargin = getExclusiveTopMarginInBursts();
        }
    } else {
        bottomMargin = kLegacyBottomMarginInBursts;
    }

    int32_t burst = stream.getFramesPerBurst();
    int32_t minSize = bottomMargin * burst;
    int32_t adjustedSize = requestedSize;
    if (adjustedSize < minSize ) {
        adjustedSize = minSize;
    } else {
        int32_t maxSize = stream.getBufferCapacityInFrames() - (topMargin * burst);
        if (adjustedSize > maxSize ) {
            adjustedSize = maxSize;
        }
    }
    return adjustedSize;
}

bool QuirksManager::DeviceQuirks::isAAudioMMapPossible(const AudioStreamBuilder &builder) const {
    bool isSampleRateCompatible =
            builder.getSampleRate() == oboe::Unspecified
            || builder.getSampleRate() == kCommonNativeRate
            || builder.getSampleRateConversionQuality() != SampleRateConversionQuality::None;
    return builder.getPerformanceMode() == PerformanceMode::LowLatency
            && isSampleRateCompatible
            && builder.getChannelCount() <= kChannelCountStereo;
}

bool QuirksManager::DeviceQuirks::shouldConvertFloatToI16ForOutputStreams() {
    std::string productManufacturer = getPropertyString("ro.product.manufacturer");
    if (getSdkVersion() < __ANDROID_API_L__) {
        return true;
    } else if ((productManufacturer == "vivo") && (getSdkVersion() < __ANDROID_API_M__)) {
        return true;
    }
    return false;
}

/**
 * This is for Samsung Exynos quirks. Samsung Mobile uses Qualcomm chips so
 * the QualcommDeviceQuirks would apply.
 */
class SamsungExynosDeviceQuirks : public  QuirksManager::DeviceQuirks {
public:
    SamsungExynosDeviceQuirks() {
        std::string chipname = getPropertyString("ro.hardware.chipname");
        isExynos9810 = (chipname == "exynos9810");
        isExynos990 = (chipname == "exynos990");
        isExynos850 = (chipname == "exynos850");

        mBuildChangelist = getPropertyInteger("ro.build.changelist", 0);
    }

    virtual ~SamsungExynosDeviceQuirks() = default;

    int32_t getExclusiveBottomMarginInBursts() const override {
        return kBottomMargin;
    }

    int32_t getExclusiveTopMarginInBursts() const override {
        return kTopMargin;
    }

    // See Oboe issues #824 and #1247 for more information.
    bool isMonoMMapActuallyStereo() const override {
        return isExynos9810 || isExynos850; // TODO We can make this version specific if it gets fixed.
    }

    bool isAAudioMMapPossible(const AudioStreamBuilder &builder) const override {
        return DeviceQuirks::isAAudioMMapPossible(builder)
                // Samsung says they use Legacy for Camcorder
                && builder.getInputPreset() != oboe::InputPreset::Camcorder;
    }

    bool isMMapSafe(const AudioStreamBuilder &builder) override {
        const bool isInput = builder.getDirection() == Direction::Input;
        // This detects b/159066712 , S20 LSI has corrupt low latency audio recording
        // and turns off MMAP.
        // See also https://github.com/google/oboe/issues/892
        bool isRecordingCorrupted = isInput
            && isExynos990
            && mBuildChangelist < 19350896;

        // Certain S9+ builds record silence when using MMAP and not using the VoiceCommunication
        // preset.
        // See https://github.com/google/oboe/issues/1110
        bool wouldRecordSilence = isInput
            && isExynos9810
            && mBuildChangelist <= 18847185
            && (builder.getInputPreset() != InputPreset::VoiceCommunication);

        if (wouldRecordSilence){
            LOGI("QuirksManager::%s() Requested stream configuration would result in silence on "
                 "this device. Switching off MMAP.", __func__);
        }

        return !isRecordingCorrupted && !wouldRecordSilence;
    }

private:
    // Stay farther away from DSP position on Exynos devices.
    static constexpr int32_t kBottomMargin = 2;
    static constexpr int32_t kTopMargin = 1;
    bool isExynos9810 = false;
    bool isExynos990 = false;
    bool isExynos850 = false;
    int mBuildChangelist = 0;
};

class QualcommDeviceQuirks : public  QuirksManager::DeviceQuirks {
public:
    QualcommDeviceQuirks() {
        std::string modelName = getPropertyString("ro.soc.model");
        isSM8150 = (modelName == "SDM8150");
    }

    virtual ~QualcommDeviceQuirks() = default;

    int32_t getExclusiveBottomMarginInBursts() const override {
        return kBottomMargin;
    }

    bool isMMapSafe(const AudioStreamBuilder &builder) override {
        // See https://github.com/google/oboe/issues/1121#issuecomment-897957749
        bool isMMapBroken = false;
        if (isSM8150 && (getSdkVersion() <= __ANDROID_API_P__)) {
            LOGI("QuirksManager::%s() MMAP not actually supported on this chip."
                 " Switching off MMAP.", __func__);
            isMMapBroken = true;
        }

        return !isMMapBroken;
    }

private:
    bool isSM8150 = false;
    static constexpr int32_t kBottomMargin = 1;
};

QuirksManager::QuirksManager() {
    std::string productManufacturer = getPropertyString("ro.product.manufacturer");
    if (productManufacturer == "samsung") {
        std::string arch = getPropertyString("ro.arch");
        bool isExynos = (arch.rfind("exynos", 0) == 0); // starts with?
        if (isExynos) {
            mDeviceQuirks = std::make_unique<SamsungExynosDeviceQuirks>();
        }
    }
    if (!mDeviceQuirks) {
        std::string socManufacturer = getPropertyString("ro.soc.manufacturer");
        if (socManufacturer == "Qualcomm") {
            // This may include Samsung Mobile devices.
            mDeviceQuirks = std::make_unique<QualcommDeviceQuirks>();
        } else {
            mDeviceQuirks = std::make_unique<DeviceQuirks>();
        }
    }
}

bool QuirksManager::isConversionNeeded(
        const AudioStreamBuilder &builder,
        AudioStreamBuilder &childBuilder) {
    bool conversionNeeded = false;
    const bool isLowLatency = builder.getPerformanceMode() == PerformanceMode::LowLatency;
    const bool isInput = builder.getDirection() == Direction::Input;
    const bool isFloat = builder.getFormat() == AudioFormat::Float;
    const bool isIEC61937 = builder.getFormat() == AudioFormat::IEC61937;
    const bool isCompressed = isCompressedFormat(builder.getFormat());

    // There should be no conversion for IEC61937. Sample rates and channel counts must be set explicitly.
    if (isIEC61937) {
        LOGI("QuirksManager::%s() conversion not needed for IEC61937", __func__);
        return false;
    }

    if (isCompressed) {
        LOGI("QuirksManager::%s() conversion not needed for compressed format %d",
             __func__, builder.getFormat());
        return false;
    }

    // There are multiple bugs involving using callback with a specified callback size.
    // Issue #778: O to Q had a problem with Legacy INPUT streams for FLOAT streams
    // and a specified callback size. It would assert because of a bad buffer size.
    //
    // Issue #973: O to R had a problem with Legacy output streams using callback and a specified callback size.
    // An AudioTrack stream could still be running when the AAudio FixedBlockReader was closed.
    // Internally b/161914201#comment25
    //
    // Issue #983: O to R would glitch if the framesPerCallback was too small.
    //
    // Most of these problems were related to Legacy stream. MMAP was OK. But we don't
    // know if we will get an MMAP stream. So, to be safe, just do the conversion in Oboe.
    if (OboeGlobals::areWorkaroundsEnabled()
            && builder.willUseAAudio()
            && builder.isDataCallbackSpecified()
            && builder.getFramesPerDataCallback() != 0
            && getSdkVersion() <= __ANDROID_API_R__) {
        LOGI("QuirksManager::%s() avoid setFramesPerCallback(n>0)", __func__);
        childBuilder.setFramesPerCallback(oboe::Unspecified);
        conversionNeeded = true;
    }

    // If a SAMPLE RATE is specified for low latency, let the native code choose an optimal rate.
    // This isn't really a workaround. It is an Oboe feature that is convenient to place here.
    // TODO There may be a problem if the devices supports low latency
    //      at a higher rate than the default.
    if (builder.getSampleRate() != oboe::Unspecified
            && builder.getSampleRateConversionQuality() != SampleRateConversionQuality::None
            && isLowLatency
            ) {
        childBuilder.setSampleRate(oboe::Unspecified); // native API decides the best sample rate
        conversionNeeded = true;
    }

    // Data Format
    // OpenSL ES and AAudio before P do not support FAST path for FLOAT capture.
    if (OboeGlobals::areWorkaroundsEnabled()
            && isFloat
            && isInput
            && builder.isFormatConversionAllowed()
            && isLowLatency
            && (!builder.willUseAAudio() || (getSdkVersion() < __ANDROID_API_P__))
            ) {
        childBuilder.setFormat(AudioFormat::I16); // needed for FAST track
        conversionNeeded = true;
        LOGI("QuirksManager::%s() forcing internal format to I16 for low latency", __func__);
    }

    // Add quirk for float output when needed.
    if (OboeGlobals::areWorkaroundsEnabled()
            && isFloat
            && !isInput
            && builder.isFormatConversionAllowed()
            && mDeviceQuirks->shouldConvertFloatToI16ForOutputStreams()
            ) {
        childBuilder.setFormat(AudioFormat::I16);
        conversionNeeded = true;
        LOGI("QuirksManager::%s() float was requested but not supported on pre-L devices "
             "and some devices like Vivo devices may have issues on L devices, "
             "creating an underlying I16 stream and using format conversion to provide a float "
             "stream", __func__);
    }

    // Channel Count conversions
    if (OboeGlobals::areWorkaroundsEnabled()
            && builder.isChannelConversionAllowed()
            && builder.getChannelCount() == kChannelCountStereo
            && isInput
            && isLowLatency
            && (!builder.willUseAAudio() && (getSdkVersion() == __ANDROID_API_O__))
            ) {
        // Workaround for heap size regression in O.
        // b/66967812 AudioRecord does not allow FAST track for stereo capture in O
        childBuilder.setChannelCount(kChannelCountMono);
        conversionNeeded = true;
        LOGI("QuirksManager::%s() using mono internally for low latency on O", __func__);
    } else if (OboeGlobals::areWorkaroundsEnabled()
               && builder.getChannelCount() == kChannelCountMono
               && isInput
               && mDeviceQuirks->isMonoMMapActuallyStereo()
               && builder.willUseAAudio()
               // Note: we might use this workaround on a device that supports
               // MMAP but will use Legacy for this stream.  But this will only happen
               // on devices that have the broken mono.
               && mDeviceQuirks->isAAudioMMapPossible(builder)
               ) {
        // Workaround for mono actually running in stereo mode.
        childBuilder.setChannelCount(kChannelCountStereo); // Use stereo and extract first channel.
        conversionNeeded = true;
        LOGI("QuirksManager::%s() using stereo internally to avoid broken mono", __func__);
    }
    // Note that MMAP does not support mono in 8.1. But that would only matter on Pixel 1
    // phones and they have almost all been updated to 9.0.

    return conversionNeeded;
}

bool QuirksManager::isMMapSafe(AudioStreamBuilder &builder) {
    if (!OboeGlobals::areWorkaroundsEnabled()) return true;
    return mDeviceQuirks->isMMapSafe(builder);
}
