/*
**
** Copyright 2014, 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 "AudioHAL_AudioHardwareOutput"

#include <utils/Log.h>

#include <stdint.h>
#include <limits.h>
#include <math.h>

#include <common_time/local_clock.h>
#include <cutils/properties.h>

#include "AudioHardwareOutput.h"
#include "AudioStreamOut.h"
#include "HDMIAudioOutput.h"

namespace android {

// Global singleton.
AudioHardwareOutput gAudioHardwareOutput;

// HDMI options.
const String8 AudioHardwareOutput::kHDMIAllowedParamKey(
        "atv.hdmi_audio.allowed");
const String8 AudioHardwareOutput::kHDMIDelayCompParamKey(
        "atv.hdmi.audio_delay");
const String8 AudioHardwareOutput::kFixedHDMIOutputParamKey(
        "atv.hdmi.fixed_volume");
const String8 AudioHardwareOutput::kFixedHDMIOutputLevelParamKey(
        "atv.hdmi.fixed_level");

// Video delay comp hack options (not exposed to user level)
const String8 AudioHardwareOutput::kVideoDelayCompParamKey(
        "atv.video.delay_comp");

// Defaults for settings.
void AudioHardwareOutput::OutputSettings::setDefaults()
{
    allowed = true;
    delayCompUsec = 0;
    isFixed = false;
    fixedLvl = 0.0f;
}

void AudioHardwareOutput::Settings::setDefaults() {
    hdmi.setDefaults();

    masterVolume = 0.60;
    masterMute = false;

    // Default this to 16mSec or so.  Since audio start times are not sync'ed to
    // to the VBI, there should be a +/-0.5 output frame rate error in the AV
    // sync, even under the best of circumstances.
    //
    // In practice, the android core seems to have a hard time hitting its frame
    // cadence consistently.  Sometimes the frames are on time, and sometimes
    // they are even a little early, but more often than not, the frames are
    // late by about a full output frame time.
    //
    // ATV pretty much always uses a 60fps output rate, and the only thing
    // consuming the latency estimate provided by the HAL is the path handling
    // AV sync.  For now, we can fudge this number to move things back in the
    // direction of correct by providing a setting for video delay compensation
    // which will be subtracted from the latency estimate and defaulting it to
    // a reasonable middle gound (12mSec in this case).
    videoDelayCompUsec = 12000;
}

AudioHardwareOutput::AudioHardwareOutput()
  : mMainOutput(NULL)
  , mMCOutput(NULL)
  , mHDMIConnected(false)
  , mMaxDelayCompUsec(0)
{
    mSettings.setDefaults();
    mHDMICardID = find_alsa_card_by_name(kHDMI_ALSADeviceName);
}

AudioHardwareOutput::~AudioHardwareOutput()
{
    closeOutputStream(mMainOutput);
    closeOutputStream(mMCOutput);
}

status_t AudioHardwareOutput::initCheck() {
    return NO_ERROR;
}

AudioStreamOut* AudioHardwareOutput::openOutputStream(
        uint32_t devices,
        audio_format_t *format,
        uint32_t *channels,
        uint32_t *sampleRate,
        audio_output_flags_t flags,
        status_t *status) {
    (void) devices;
    AutoMutex lock(mStreamLock);

    AudioStreamOut** pp_out;
    AudioStreamOut* out;

    bool isIec958NonAudio = (flags & AUDIO_OUTPUT_FLAG_IEC958_NONAUDIO) != 0;
    if (!(flags & AUDIO_OUTPUT_FLAG_DIRECT)) {
        pp_out = &mMainOutput;
        out = new AudioStreamOut(*this, false, isIec958NonAudio);
    } else {
        pp_out = &mMCOutput;
        out = new AudioStreamOut(*this, true, isIec958NonAudio);
    }

    if (out == NULL) {
        *status = NO_MEMORY;
        return NULL;
    }

    *status = out->set(format, channels, sampleRate);

    if (*status == NO_ERROR) {
        *pp_out = out;
        updateTgtDevices_l();
    } else {
        delete out;
    }

    return *pp_out;
}

void AudioHardwareOutput::closeOutputStream(AudioStreamOut* out) {
    if (out == NULL)
        return;

    // Putting the stream into "standby" should cause it to release all of its
    // physical outputs.
    out->standby();

    {
        Mutex::Autolock _l(mStreamLock);
        if (mMainOutput && out == mMainOutput) {
            delete mMainOutput;
            mMainOutput = NULL;
        } else if (mMCOutput && out == mMCOutput) {
            delete mMCOutput;
            mMCOutput = NULL;
        }

        updateTgtDevices_l();
    }
}

status_t AudioHardwareOutput::setMasterVolume(float volume)
{
    Mutex::Autolock _l1(mOutputLock);
    Mutex::Autolock _l2(mSettingsLock);

    mSettings.masterVolume = volume;

    AudioOutputList::iterator I;
    for (I = mPhysOutputs.begin(); I != mPhysOutputs.end(); ++I)
        (*I)->setVolume(mSettings.masterVolume);

    return NO_ERROR;
}

status_t AudioHardwareOutput::getMasterVolume(float* volume) {

    if (NULL == volume)
        return BAD_VALUE;

    // Explicit scope for auto-lock pattern.
    {
        Mutex::Autolock _l(mSettingsLock);
        *volume = mSettings.masterVolume;
    }

    return NO_ERROR;
}

status_t AudioHardwareOutput::setMasterMute(bool muted)
{
    Mutex::Autolock _l1(mOutputLock);
    Mutex::Autolock _l2(mSettingsLock);

    mSettings.masterMute = muted;

    AudioOutputList::iterator I;
    for (I = mPhysOutputs.begin(); I != mPhysOutputs.end(); ++I)
        (*I)->setMute(mSettings.masterMute);

    return NO_ERROR;
}

status_t AudioHardwareOutput::getMasterMute(bool* muted) {
    if (NULL == muted)
        return BAD_VALUE;

    // Explicit scope for auto-lock pattern.
    {
        Mutex::Autolock _l(mSettingsLock);
        *muted = mSettings.masterMute;
    }

    return NO_ERROR;
}

status_t AudioHardwareOutput::setParameters(const char* kvpairs) {
    AudioParameter param = AudioParameter(String8(kvpairs));
    status_t status = NO_ERROR;
    float floatVal;
    int intVal;
    Settings initial, s;

    {
        // Record the initial state of the settings from inside the lock.  Then
        // leave the lock in order to parse the changes to be made.
        Mutex::Autolock _l(mSettingsLock);
        initial = s = mSettings;
    }

    /***************************************************************
     *                     HDMI Audio Options                      *
     ***************************************************************/
    if (param.getInt(kHDMIAllowedParamKey, intVal) == NO_ERROR) {
        s.hdmi.allowed = (intVal != 0);
        param.remove(kHDMIAllowedParamKey);
    }

    if ((param.getFloat(kHDMIDelayCompParamKey, floatVal) == NO_ERROR) &&
        (floatVal >= 0.0) &&
        (floatVal <= AudioOutput::kMaxDelayCompensationMSec)) {
        uint32_t delay_comp = static_cast<uint32_t>(floatVal * 1000.0);
        s.hdmi.delayCompUsec = delay_comp;
        param.remove(kHDMIDelayCompParamKey);
    }

    if (param.getInt(kFixedHDMIOutputParamKey, intVal) == NO_ERROR) {
        s.hdmi.isFixed = (intVal != 0);
        param.remove(kFixedHDMIOutputParamKey);
    }

    if ((param.getFloat(kFixedHDMIOutputLevelParamKey, floatVal) == NO_ERROR)
        && (floatVal <= 0.0)) {
        s.hdmi.fixedLvl = floatVal;
        param.remove(kFixedHDMIOutputLevelParamKey);
    }

    /***************************************************************
     *                       Other Options                         *
     ***************************************************************/
    if ((param.getFloat(kVideoDelayCompParamKey, floatVal) == NO_ERROR) &&
        (floatVal >= 0.0) &&
        (floatVal <= AudioOutput::kMaxDelayCompensationMSec)) {
        s.videoDelayCompUsec = static_cast<uint32_t>(floatVal * 1000.0);
        param.remove(kVideoDelayCompParamKey);
    }

    if (param.size())
        status = BAD_VALUE;

    // If there was a change made to settings, go ahead and apply it now.
    bool allowedOutputsChanged = false;
    if (memcmp(&initial, &s, sizeof(initial)))  {
        Mutex::Autolock _l1(mOutputLock);
        Mutex::Autolock _l2(mSettingsLock);

        if (memcmp(&initial.hdmi, &s.hdmi, sizeof(initial.hdmi)))
            allowedOutputsChanged = allowedOutputsChanged ||
                applyOutputSettings_l(initial.hdmi, s.hdmi, mSettings.hdmi,
                                      HDMIAudioOutput::classDevMask());

        if (initial.videoDelayCompUsec != s.videoDelayCompUsec)
            mSettings.videoDelayCompUsec = s.videoDelayCompUsec;

        uint32_t tmp = 0;
        if (mSettings.hdmi.allowed && (tmp < mSettings.hdmi.delayCompUsec))
            tmp = mSettings.hdmi.delayCompUsec;
        if (mMaxDelayCompUsec != tmp)
            mMaxDelayCompUsec = tmp;
    }

    if (allowedOutputsChanged) {
        Mutex::Autolock _l(mStreamLock);
        updateTgtDevices_l();
    }

    return status;
}

bool AudioHardwareOutput::applyOutputSettings_l(
        const AudioHardwareOutput::OutputSettings& initial,
        const AudioHardwareOutput::OutputSettings& current,
        AudioHardwareOutput::OutputSettings& updateMe,
        uint32_t outDevMask) {
    // ASSERT(holding mOutputLock and mSettingsLock)
    sp<AudioOutput> out;

    // Check for a change in the allowed/not-allowed state.  Update if needed
    // and return true if there was a change made.
    bool ret = false;
    if (initial.allowed != current.allowed) {
        updateMe.allowed = current.allowed;
        ret = true;
    }

    // Look for an instance of the output to be updated in case other changes
    // were made.
    AudioOutputList::iterator I;
    for (I = mPhysOutputs.begin(); I != mPhysOutputs.end(); ++I) {
        if (outDevMask == (*I)->devMask()) {
            out = (*I);
            break;
        }
    }

    // Update the other settings, if needed.
    if (initial.delayCompUsec != current.delayCompUsec) {
        updateMe.delayCompUsec = current.delayCompUsec;
        if (out != NULL)
            out->setExternalDelay_uSec(current.delayCompUsec);
    }

    if (initial.isFixed != current.isFixed) {
        updateMe.isFixed = current.isFixed;
        if (out != NULL)
            out->setOutputIsFixed(current.isFixed);
    }

    if (initial.fixedLvl != current.fixedLvl) {
        updateMe.fixedLvl = current.fixedLvl;
        if (out != NULL)
            out->setFixedOutputLevel(current.fixedLvl);
    }

    return ret;
}


char* AudioHardwareOutput::getParameters(const char* keys) {
    Settings s;

    // Explicit scope for auto-lock pattern.
    {
        // Snapshot the current settings so we don't have to hold the settings
        // lock while formatting the results.
        Mutex::Autolock _l(mSettingsLock);
        s = mSettings;
    }

    AudioParameter param = AudioParameter(String8(keys));
    String8 tmp;

    /***************************************************************
     *                     HDMI Audio Options                      *
     ***************************************************************/
    if (param.get(kHDMIAllowedParamKey, tmp) == NO_ERROR)
        param.addInt(kHDMIAllowedParamKey, s.hdmi.allowed ? 1 : 0);

    if (param.get(kHDMIDelayCompParamKey, tmp) == NO_ERROR)
        param.addFloat(kHDMIDelayCompParamKey,
                       static_cast<float>(s.hdmi.delayCompUsec) / 1000.0);

    if (param.get(kFixedHDMIOutputParamKey, tmp) == NO_ERROR)
        param.addInt(kFixedHDMIOutputParamKey, s.hdmi.isFixed ? 1 : 0);

    if (param.get(kFixedHDMIOutputLevelParamKey, tmp) == NO_ERROR)
        param.addFloat(kFixedHDMIOutputLevelParamKey, s.hdmi.fixedLvl);

    /***************************************************************
     *                       Other Options                         *
     ***************************************************************/
    if (param.get(kVideoDelayCompParamKey, tmp) == NO_ERROR)
        param.addFloat(kVideoDelayCompParamKey,
                       static_cast<float>(s.videoDelayCompUsec) / 1000.0);

    return strdup(param.toString().string());
}

void AudioHardwareOutput::updateRouting(uint32_t devMask) {
    Mutex::Autolock _l(mStreamLock);

    bool hasHDMI = 0 != (devMask & HDMIAudioOutput::classDevMask());
    ALOGI("%s: hasHDMI = %d, mHDMIConnected = %d", __func__, hasHDMI, mHDMIConnected);
    if (mHDMIConnected != hasHDMI) {
        mHDMIConnected = hasHDMI;

        if (mHDMIConnected)
            mHDMIAudioCaps.loadCaps(mHDMICardID);
        else
            mHDMIAudioCaps.reset();

        updateTgtDevices_l();
    }
}

status_t AudioHardwareOutput::obtainOutput(const AudioStreamOut& tgtStream,
                                     uint32_t devMask,
                                     sp<AudioOutput>* newOutput) {
    Mutex::Autolock _l1(mOutputLock);

    // Sanity check the device mask passed to us.  There should exactly one bit
    // set, no less, no more.
    if (popcount(devMask) != 1) {
        ALOGW("bad device mask in obtainOutput, %08x", devMask);
        return INVALID_OPERATION;
    }

    // Start by checking to see if the requested output is currently busy.
    AudioOutputList::iterator I;
    for (I = mPhysOutputs.begin(); I != mPhysOutputs.end(); ++I)
        if (devMask & (*I)->devMask())
            return OK; // Yup; its busy.

    // Looks like we don't currently have an output of the requested type.
    // Figure out which type is being requested and try to construct one.
    OutputSettings* S = NULL;
    if (devMask & HDMIAudioOutput::classDevMask()) {
        *newOutput = new HDMIAudioOutput();
        S = &mSettings.hdmi;
    }
    else {
        ALOGW("%s stream out requested output of unknown type %08x",
                tgtStream.getName(), devMask);
        return BAD_VALUE;
    }

    if (*newOutput == NULL)
        return NO_MEMORY;

    status_t res = (*newOutput)->setupForStream(tgtStream);
    if (res != OK) {
        ALOGE("%s setupForStream() returned %d",
              tgtStream.getName(), res);
        *newOutput = NULL;
    } else {
        ALOGI("%s stream out adding %s output.",
                tgtStream.getName(), (*newOutput)->getOutputName());
        mPhysOutputs.push_back(*newOutput);

        {  // Apply current settings
            Mutex::Autolock _l2(mSettingsLock);
            (*newOutput)->setVolume(mSettings.masterVolume);
            (*newOutput)->setMute(mSettings.masterMute);
            (*newOutput)->setExternalDelay_uSec(S->delayCompUsec);
            (*newOutput)->setOutputIsFixed(S->isFixed);
            (*newOutput)->setFixedOutputLevel(S->fixedLvl);
        }
    }

    return res;
}

void AudioHardwareOutput::releaseOutput(const AudioStreamOut& tgtStream,
                                        const sp<AudioOutput>& releaseMe) {
    Mutex::Autolock _l(mOutputLock);

    ALOGI("%s stream out removing %s output.",
            tgtStream.getName(), releaseMe->getOutputName());

    // Immediately release any resources associated with this output (In
    // particular, make sure to close any ALSA device driver handles ASAP)
    releaseMe->cleanupResources();

    // Now, clear our internal bookkeeping.
    AudioOutputList::iterator I;
    for (I = mPhysOutputs.begin(); I != mPhysOutputs.end(); ++I) {
        if (releaseMe.get() == (*I).get()) {
            mPhysOutputs.erase(I);
            break;
        }
    }
}

void AudioHardwareOutput::updateTgtDevices_l() {
    // ASSERT(holding mStreamLock)
    uint32_t mcMask = 0;
    uint32_t mainMask = 0;

    {
        Mutex::Autolock _l(mSettingsLock);
        if (mSettings.hdmi.allowed && mHDMIConnected) {
            if (NULL != mMCOutput)
                mcMask |= HDMIAudioOutput::classDevMask();
            else
                mainMask |= HDMIAudioOutput::classDevMask();
        }
    }

    if (NULL != mMainOutput)
        mMainOutput->setTgtDevices(mainMask);

    if (NULL != mMCOutput)
        mMCOutput->setTgtDevices(mcMask);
}

void AudioHardwareOutput::standbyStatusUpdate(bool isInStandby, bool isMCStream) {

    Mutex::Autolock _l1(mStreamLock);
    bool hdmiAllowed;
    {
        Mutex::Autolock _l2(mSettingsLock);
        hdmiAllowed = mSettings.hdmi.allowed;
    }
    // If there is no HDMI, do nothing
    if (hdmiAllowed && mHDMIConnected) {
        // If a multi-channel stream goes to standy state, we must switch
        // to stereo stream. If MC comes out of standby, we must switch
        // back to MC. No special processing needed for main stream.
        // AudioStreamOut class handles that correctly
        if (isMCStream) {
            uint32_t mcMask;
            uint32_t mainMask;
            if (isInStandby) {
                mainMask = HDMIAudioOutput::classDevMask();
                mcMask = 0;
            } else {
                mainMask = 0;
                mcMask = HDMIAudioOutput::classDevMask();
            }

            if (NULL != mMainOutput)
                mMainOutput->setTgtDevices(mainMask);

            if (NULL != mMCOutput)
                mMCOutput->setTgtDevices(mcMask);
        }
    }
}

#define DUMP(a...) \
    snprintf(buffer, SIZE, a); \
    buffer[SIZE - 1] = 0; \
    result.append(buffer);
#define B2STR(b) b ? "true" : "false"

status_t AudioHardwareOutput::dump(int fd)
{
    const size_t SIZE = 256;
    char buffer[SIZE];
    String8 result;
    Settings s;

    // Explicit scope for auto-lock pattern.
    {
        // Snapshot the current settings so we don't have to hold the settings
        // lock while formatting the results.
        Mutex::Autolock _l(mSettingsLock);
        s = mSettings;
    }

    DUMP("AudioHardwareOutput::dump\n");
    DUMP("\tMaster Volume          : %0.3f\n", s.masterVolume);
    DUMP("\tMaster Mute            : %s\n", B2STR(s.masterMute));
    DUMP("\tHDMI Output Allowed    : %s\n", B2STR(s.hdmi.allowed));
    DUMP("\tHDMI Delay Comp        : %u uSec\n", s.hdmi.delayCompUsec);
    DUMP("\tHDMI Output Fixed      : %s\n", B2STR(s.hdmi.isFixed));
    DUMP("\tHDMI Fixed Level       : %.1f dB\n", s.hdmi.fixedLvl);
    DUMP("\tVideo Delay Comp       : %u uSec\n", s.videoDelayCompUsec);

    ::write(fd, result.string(), result.size());

    // Explicit scope for auto-lock pattern.
    {
        Mutex::Autolock _l(mOutputLock);
        if (mMainOutput)
            mMainOutput->dump(fd);

        if (mMCOutput)
            mMCOutput->dump(fd);
    }

    return NO_ERROR;
}

#undef B2STR
#undef DUMP

}; // namespace android