/*
 * 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 "common/OboeDebug.h"
#include "FullDuplexStream.h"

oboe::ResultWithValue<int32_t>  FullDuplexStream::readInput(int32_t numFrames) {
    oboe::ResultWithValue<int32_t> result = getInputStream()->read(
            mInputConverter->getInputBuffer(),
            numFrames,
            0 /* timeout */);
    if (result == oboe::Result::OK) {
        int32_t numSamples = result.value() * getInputStream()->getChannelCount();
        mInputConverter->convertInternalBuffers(numSamples);
    }
    return result;
}

oboe::DataCallbackResult FullDuplexStream::onAudioReady(
        oboe::AudioStream *outputStream,
        void *audioData,
        int numFrames) {
    oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue;
    int32_t actualFramesRead = 0;

    // Silence the output.
    int32_t numBytes = numFrames * outputStream->getBytesPerFrame();
    memset(audioData, 0 /* value */, numBytes);

    if (mCountCallbacksToDrain > 0) {
        // Drain the input.
        int32_t totalFramesRead = 0;
        do {
            oboe::ResultWithValue<int32_t> result = readInput(numFrames);
            if (!result) {
                // Ignore errors because input stream may not be started yet.
                break;
            }
            actualFramesRead = result.value();
            totalFramesRead += actualFramesRead;
        } while (actualFramesRead > 0);
        // Only counts if we actually got some data.
        if (totalFramesRead > 0) {
            mCountCallbacksToDrain--;
        }

    } else if (mCountInputBurstsCushion > 0) {
        // Let the input fill up a bit so we are not so close to the write pointer.
        mCountInputBurstsCushion--;

    } else if (mCountCallbacksToDiscard > 0) {
        mCountCallbacksToDiscard--;
        // Ignore. Allow the input to reach to equilibrium with the output.
        oboe::ResultWithValue<int32_t> resultAvailable = getInputStream()->getAvailableFrames();
        if (!resultAvailable) {
            LOGE("%s() getAvailableFrames() returned %s\n",
                    __func__, convertToText(resultAvailable.error()));
            callbackResult = oboe::DataCallbackResult::Stop;
        } else {
            int32_t framesAvailable = resultAvailable.value();
            if (framesAvailable >= mMinimumFramesBeforeRead) {
                oboe::ResultWithValue<int32_t> resultRead = readInput(numFrames);
                if (!resultRead) {
                    LOGE("%s() read() returned %s\n", __func__, convertToText(resultRead.error()));
                    callbackResult = oboe::DataCallbackResult::Stop;
                }
            }
        }
    } else {
        int32_t framesRead = 0;
        oboe::ResultWithValue<int32_t> resultAvailable = getInputStream()->getAvailableFrames();
        if (!resultAvailable) {
            LOGE("%s() getAvailableFrames() returned %s\n", __func__, convertToText(resultAvailable.error()));
            callbackResult = oboe::DataCallbackResult::Stop;
        } else {
            int32_t framesAvailable = resultAvailable.value();
            if (framesAvailable >= mMinimumFramesBeforeRead) {
                // Read data into input buffer.
                oboe::ResultWithValue<int32_t> resultRead = readInput(numFrames);
                if (!resultRead) {
                    LOGE("%s() read() returned %s\n", __func__, convertToText(resultRead.error()));
                    callbackResult = oboe::DataCallbackResult::Stop;
                } else {
                    framesRead = resultRead.value();
                }
            }
        }

        if (callbackResult == oboe::DataCallbackResult::Continue) {
            callbackResult = onBothStreamsReady(
                    (const float *) mInputConverter->getOutputBuffer(),
                    framesRead,
                    (float *) mOutputConverter->getInputBuffer(), numFrames);
            mOutputConverter->convertFromInternalInput( audioData,
                                       numFrames * getOutputStream()->getChannelCount());
        }
    }

    if (callbackResult == oboe::DataCallbackResult::Stop) {
        getInputStream()->requestStop();
    }

    return callbackResult;
}

oboe::Result FullDuplexStream::start() {
    mCountCallbacksToDrain = kNumCallbacksToDrain;
    mCountInputBurstsCushion = mNumInputBurstsCushion;
    mCountCallbacksToDiscard = kNumCallbacksToDiscard;

    // Determine maximum size that could possibly be called.
    int32_t bufferSize = getOutputStream()->getBufferCapacityInFrames()
            * getOutputStream()->getChannelCount();
    mInputConverter = std::make_unique<FormatConverterBox>(bufferSize,
            getInputStream()->getFormat(),
            oboe::AudioFormat::Float);
    mOutputConverter = std::make_unique<FormatConverterBox>(bufferSize,
            oboe::AudioFormat::Float,
            getOutputStream()->getFormat());

    oboe::Result result = getInputStream()->requestStart();
    if (result != oboe::Result::OK) {
        return result;
    }
    return getOutputStream()->requestStart();
}

oboe::Result FullDuplexStream::stop() {
    getOutputStream()->requestStop(); // TODO result?
    return getInputStream()->requestStop();
}

int32_t FullDuplexStream::getMNumInputBurstsCushion() const {
    return mNumInputBurstsCushion;
}

void FullDuplexStream::setMNumInputBurstsCushion(int32_t numBursts) {
    FullDuplexStream::mNumInputBurstsCushion = numBursts;
}
