/*
 * Copyright (C) 2023 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 "AHAL_ModuleBluetooth"

#include <android-base/logging.h>

#include "BluetoothAudioSession.h"
#include "core-impl/ModuleBluetooth.h"
#include "core-impl/StreamBluetooth.h"

using aidl::android::hardware::audio::common::SinkMetadata;
using aidl::android::hardware::audio::common::SourceMetadata;
using aidl::android::hardware::bluetooth::audio::ChannelMode;
using aidl::android::hardware::bluetooth::audio::PcmConfiguration;
using aidl::android::media::audio::common::AudioChannelLayout;
using aidl::android::media::audio::common::AudioConfigBase;
using aidl::android::media::audio::common::AudioDeviceDescription;
using aidl::android::media::audio::common::AudioDeviceType;
using aidl::android::media::audio::common::AudioFormatDescription;
using aidl::android::media::audio::common::AudioFormatType;
using aidl::android::media::audio::common::AudioIoFlags;
using aidl::android::media::audio::common::AudioOffloadInfo;
using aidl::android::media::audio::common::AudioPort;
using aidl::android::media::audio::common::AudioPortConfig;
using aidl::android::media::audio::common::AudioPortExt;
using aidl::android::media::audio::common::AudioProfile;
using aidl::android::media::audio::common::Int;
using aidl::android::media::audio::common::MicrophoneInfo;
using aidl::android::media::audio::common::PcmType;
using android::bluetooth::audio::aidl::BluetoothAudioPortAidl;
using android::bluetooth::audio::aidl::BluetoothAudioPortAidlIn;
using android::bluetooth::audio::aidl::BluetoothAudioPortAidlOut;

// TODO(b/312265159) bluetooth audio should be in its own process
// Remove this and the shared_libs when that happens
extern "C" binder_status_t createIBluetoothAudioProviderFactory();

namespace aidl::android::hardware::audio::core {

namespace {

PcmType pcmTypeFromBitsPerSample(int8_t bitsPerSample) {
    if (bitsPerSample == 8)
        return PcmType::UINT_8_BIT;
    else if (bitsPerSample == 16)
        return PcmType::INT_16_BIT;
    else if (bitsPerSample == 24)
        return PcmType::INT_24_BIT;
    else if (bitsPerSample == 32)
        return PcmType::INT_32_BIT;
    ALOGE("Unsupported bitsPerSample: %d", bitsPerSample);
    return PcmType::DEFAULT;
}

AudioChannelLayout channelLayoutFromChannelMode(ChannelMode mode) {
    if (mode == ChannelMode::MONO) {
        return AudioChannelLayout::make<AudioChannelLayout::layoutMask>(
                AudioChannelLayout::LAYOUT_MONO);
    } else if (mode == ChannelMode::STEREO || mode == ChannelMode::DUALMONO) {
        return AudioChannelLayout::make<AudioChannelLayout::layoutMask>(
                AudioChannelLayout::LAYOUT_STEREO);
    }
    ALOGE("Unsupported channel mode: %s", toString(mode).c_str());
    return AudioChannelLayout{};
}

}  // namespace

ModuleBluetooth::ModuleBluetooth(std::unique_ptr<Module::Configuration>&& config)
    : Module(Type::BLUETOOTH, std::move(config)) {
    // TODO(b/312265159) bluetooth audio should be in its own process
    // Remove this and the shared_libs when that happens
    binder_status_t status = createIBluetoothAudioProviderFactory();
    if (status != STATUS_OK) {
        LOG(ERROR) << "Failed to create bluetooth audio provider factory. Status: "
                   << ::android::statusToString(status);
    }
}

ndk::ScopedAStatus ModuleBluetooth::getBluetoothA2dp(
        std::shared_ptr<IBluetoothA2dp>* _aidl_return) {
    *_aidl_return = getBtA2dp().getInstance();
    LOG(DEBUG) << __func__ << ": returning instance of IBluetoothA2dp: " << _aidl_return->get();
    return ndk::ScopedAStatus::ok();
}

ndk::ScopedAStatus ModuleBluetooth::getBluetoothLe(std::shared_ptr<IBluetoothLe>* _aidl_return) {
    *_aidl_return = getBtLe().getInstance();
    LOG(DEBUG) << __func__ << ": returning instance of IBluetoothLe: " << _aidl_return->get();
    return ndk::ScopedAStatus::ok();
}

ChildInterface<BluetoothA2dp>& ModuleBluetooth::getBtA2dp() {
    if (!mBluetoothA2dp) {
        auto handle = ndk::SharedRefBase::make<BluetoothA2dp>();
        handle->registerHandler(std::bind(&ModuleBluetooth::bluetoothParametersUpdated, this));
        mBluetoothA2dp = handle;
    }
    return mBluetoothA2dp;
}

ChildInterface<BluetoothLe>& ModuleBluetooth::getBtLe() {
    if (!mBluetoothLe) {
        auto handle = ndk::SharedRefBase::make<BluetoothLe>();
        handle->registerHandler(std::bind(&ModuleBluetooth::bluetoothParametersUpdated, this));
        mBluetoothLe = handle;
    }
    return mBluetoothLe;
}

ModuleBluetooth::BtProfileHandles ModuleBluetooth::getBtProfileManagerHandles() {
    return std::make_tuple(std::weak_ptr<IBluetooth>(), getBtA2dp().getPtr(), getBtLe().getPtr());
}

ndk::ScopedAStatus ModuleBluetooth::getMicMute(bool* _aidl_return __unused) {
    LOG(DEBUG) << __func__ << ": is not supported";
    return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
}

ndk::ScopedAStatus ModuleBluetooth::setMicMute(bool in_mute __unused) {
    LOG(DEBUG) << __func__ << ": is not supported";
    return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
}

ndk::ScopedAStatus ModuleBluetooth::setAudioPortConfig(const AudioPortConfig& in_requested,
                                                       AudioPortConfig* out_suggested,
                                                       bool* _aidl_return) {
    auto fillConfig = [this](const AudioPort& port, AudioPortConfig* config) {
        if (port.ext.getTag() == AudioPortExt::device) {
            CachedProxy proxy;
            auto status = findOrCreateProxy(port, proxy);
            if (status.isOk()) {
                const auto& pcmConfig = proxy.pcmConfig;
                LOG(DEBUG) << "setAudioPortConfig: suggesting port config from "
                           << pcmConfig.toString();
                const auto pcmType = pcmTypeFromBitsPerSample(pcmConfig.bitsPerSample);
                const auto channelMask = channelLayoutFromChannelMode(pcmConfig.channelMode);
                if (pcmType != PcmType::DEFAULT && channelMask != AudioChannelLayout{}) {
                    config->format =
                            AudioFormatDescription{.type = AudioFormatType::PCM, .pcm = pcmType};
                    config->channelMask = channelMask;
                    config->sampleRate = Int{.value = pcmConfig.sampleRateHz};
                    config->flags = port.flags;
                    config->ext = port.ext;
                    return true;
                }
            }
        }
        return generateDefaultPortConfig(port, config);
    };
    return Module::setAudioPortConfigImpl(in_requested, fillConfig, out_suggested, _aidl_return);
}

ndk::ScopedAStatus ModuleBluetooth::checkAudioPatchEndpointsMatch(
        const std::vector<AudioPortConfig*>& sources, const std::vector<AudioPortConfig*>& sinks) {
    // Both sources and sinks must be non-empty, this is guaranteed by 'setAudioPatch'.
    const bool isInput = sources[0]->ext.getTag() == AudioPortExt::device;
    const int32_t devicePortId = isInput ? sources[0]->portId : sinks[0]->portId;
    const auto proxyIt = mProxies.find(devicePortId);
    if (proxyIt == mProxies.end()) return ndk::ScopedAStatus::ok();
    const auto& pcmConfig = proxyIt->second.pcmConfig;
    const AudioPortConfig* mixPortConfig = isInput ? sinks[0] : sources[0];
    if (!StreamBluetooth::checkConfigParams(
                pcmConfig, AudioConfigBase{.sampleRate = mixPortConfig->sampleRate->value,
                                           .channelMask = *(mixPortConfig->channelMask),
                                           .format = *(mixPortConfig->format)})) {
        return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
    }
    if (int32_t handle = mixPortConfig->ext.get<AudioPortExt::mix>().handle; handle > 0) {
        mConnections.insert(std::pair(handle, devicePortId));
    }
    return ndk::ScopedAStatus::ok();
}

void ModuleBluetooth::onExternalDeviceConnectionChanged(const AudioPort& audioPort,
                                                        bool connected) {
    if (!connected) mProxies.erase(audioPort.id);
}

ndk::ScopedAStatus ModuleBluetooth::createInputStream(
        StreamContext&& context, const SinkMetadata& sinkMetadata,
        const std::vector<MicrophoneInfo>& microphones, std::shared_ptr<StreamIn>* result) {
    CachedProxy proxy;
    RETURN_STATUS_IF_ERROR(fetchAndCheckProxy(context, proxy));
    return createStreamInstance<StreamInBluetooth>(result, std::move(context), sinkMetadata,
                                                   microphones, getBtProfileManagerHandles(),
                                                   proxy.ptr, proxy.pcmConfig);
}

ndk::ScopedAStatus ModuleBluetooth::createOutputStream(
        StreamContext&& context, const SourceMetadata& sourceMetadata,
        const std::optional<AudioOffloadInfo>& offloadInfo, std::shared_ptr<StreamOut>* result) {
    CachedProxy proxy;
    RETURN_STATUS_IF_ERROR(fetchAndCheckProxy(context, proxy));
    return createStreamInstance<StreamOutBluetooth>(result, std::move(context), sourceMetadata,
                                                    offloadInfo, getBtProfileManagerHandles(),
                                                    proxy.ptr, proxy.pcmConfig);
}

ndk::ScopedAStatus ModuleBluetooth::populateConnectedDevicePort(AudioPort* audioPort,
                                                                int32_t nextPortId) {
    if (audioPort->ext.getTag() != AudioPortExt::device) {
        LOG(ERROR) << __func__ << ": not a device port: " << audioPort->toString();
        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
    }
    if (!::aidl::android::hardware::bluetooth::audio::BluetoothAudioSession::IsAidlAvailable()) {
        LOG(ERROR) << __func__ << ": IBluetoothAudioProviderFactory AIDL service not available";
        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
    }
    const auto& devicePort = audioPort->ext.get<AudioPortExt::device>();
    const auto& description = devicePort.device.type;
    // This method must return an error when the device can not be connected.
    // Since A2DP/LE status events are sent asynchronously, it is more reliable
    // to attempt connecting to the BT stack rather than judge by the A2DP/LE status.
    if (description.connection != AudioDeviceDescription::CONNECTION_BT_A2DP &&
        description.connection != AudioDeviceDescription::CONNECTION_BT_LE &&
        !(description.connection == AudioDeviceDescription::CONNECTION_WIRELESS &&
          description.type == AudioDeviceType::OUT_HEARING_AID)) {
        LOG(ERROR) << __func__ << ": unsupported device type: " << audioPort->toString();
        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
    }
    CachedProxy proxy;
    RETURN_STATUS_IF_ERROR(createProxy(*audioPort, nextPortId, proxy));
    // If the device is actually connected, it is configured by the BT stack.
    // Provide the current configuration instead of all possible profiles.
    const auto& pcmConfig = proxy.pcmConfig;
    audioPort->profiles.clear();
    audioPort->profiles.push_back(
            AudioProfile{.format = AudioFormatDescription{.type = AudioFormatType::PCM,
                                                          .pcm = pcmTypeFromBitsPerSample(
                                                                  pcmConfig.bitsPerSample)},
                         .channelMasks = std::vector<AudioChannelLayout>(
                                 {channelLayoutFromChannelMode(pcmConfig.channelMode)}),
                         .sampleRates = std::vector<int>({pcmConfig.sampleRateHz})});
    LOG(DEBUG) << __func__ << ": " << audioPort->toString();
    return ndk::ScopedAStatus::ok();
}

ndk::ScopedAStatus ModuleBluetooth::onMasterMuteChanged(bool) {
    LOG(DEBUG) << __func__ << ": is not supported";
    return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
}

ndk::ScopedAStatus ModuleBluetooth::onMasterVolumeChanged(float) {
    LOG(DEBUG) << __func__ << ": is not supported";
    return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
}

int32_t ModuleBluetooth::getNominalLatencyMs(const AudioPortConfig& portConfig) {
    const auto connectionsIt = mConnections.find(portConfig.ext.get<AudioPortExt::mix>().handle);
    if (connectionsIt != mConnections.end()) {
        const auto proxyIt = mProxies.find(connectionsIt->second);
        if (proxyIt != mProxies.end()) {
            auto proxy = proxyIt->second.ptr;
            size_t dataIntervalUs = 0;
            if (!proxy->getPreferredDataIntervalUs(dataIntervalUs)) {
                LOG(WARNING) << __func__ << ": could not fetch preferred data interval";
            }
            const bool isInput = portConfig.flags->getTag() == AudioIoFlags::input;
            return isInput ? StreamInBluetooth::getNominalLatencyMs(dataIntervalUs)
                           : StreamOutBluetooth::getNominalLatencyMs(dataIntervalUs);
        }
    }
    LOG(ERROR) << __func__ << ": no connection or proxy found for " << portConfig.toString();
    return Module::getNominalLatencyMs(portConfig);
}

ndk::ScopedAStatus ModuleBluetooth::createProxy(const AudioPort& audioPort, int32_t instancePortId,
                                                CachedProxy& proxy) {
    const bool isInput = audioPort.flags.getTag() == AudioIoFlags::input;
    proxy.ptr = isInput ? std::shared_ptr<BluetoothAudioPortAidl>(
                                  std::make_shared<BluetoothAudioPortAidlIn>())
                        : std::shared_ptr<BluetoothAudioPortAidl>(
                                  std::make_shared<BluetoothAudioPortAidlOut>());
    const auto& devicePort = audioPort.ext.get<AudioPortExt::device>();
    const auto device = devicePort.device.type;
    bool registrationSuccess = false;
    for (int i = 0; i < kCreateProxyRetries && !registrationSuccess; ++i) {
        registrationSuccess = proxy.ptr->registerPort(device);
        usleep(kCreateProxyRetrySleepMs * 1000);
    }
    if (!registrationSuccess) {
        LOG(ERROR) << __func__ << ": failed to register BT port for " << device.toString();
        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
    }
    if (!proxy.ptr->loadAudioConfig(proxy.pcmConfig)) {
        LOG(ERROR) << __func__ << ": state=" << proxy.ptr->getState()
                   << ", failed to load audio config";
        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
    }
    mProxies.insert(std::pair(instancePortId, proxy));
    return ndk::ScopedAStatus::ok();
}

ndk::ScopedAStatus ModuleBluetooth::fetchAndCheckProxy(const StreamContext& context,
                                                       CachedProxy& proxy) {
    const auto connectionsIt = mConnections.find(context.getMixPortHandle());
    if (connectionsIt != mConnections.end()) {
        const auto proxyIt = mProxies.find(connectionsIt->second);
        if (proxyIt != mProxies.end()) {
            proxy = proxyIt->second;
            mProxies.erase(proxyIt);
        }
        mConnections.erase(connectionsIt);
    }
    if (proxy.ptr != nullptr) {
        if (!StreamBluetooth::checkConfigParams(
                    proxy.pcmConfig, AudioConfigBase{.sampleRate = context.getSampleRate(),
                                                     .channelMask = context.getChannelLayout(),
                                                     .format = context.getFormat()})) {
            return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
        }
    }
    // Not having a proxy is OK, it may happen in VTS tests when streams are opened on unconnected
    // mix ports.
    return ndk::ScopedAStatus::ok();
}

ndk::ScopedAStatus ModuleBluetooth::findOrCreateProxy(const AudioPort& audioPort,
                                                      CachedProxy& proxy) {
    if (auto proxyIt = mProxies.find(audioPort.id); proxyIt != mProxies.end()) {
        proxy = proxyIt->second;
        return ndk::ScopedAStatus::ok();
    }
    return createProxy(audioPort, audioPort.id, proxy);
}

}  // namespace aidl::android::hardware::audio::core
