/*
 * Copyright (C) 2017 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.
 */

// Try to trigger bugs by playing randomly on multiple streams.

#include <stdio.h>
#include <stdlib.h>
#include <vector>

#include <aaudio/AAudio.h>
#include "AAudioArgsParser.h"
#include "AAudioExampleUtils.h"
#include "AAudioSimplePlayer.h"
#include "SineGenerator.h"

#define DEFAULT_TIMEOUT_NANOS  (1 * NANOS_PER_SECOND)

#define NUM_LOOPS          1000
#define MAX_MICROS_DELAY   (2 * 1000 * 1000)

// TODO Consider adding an input stream.
#define PROB_START   (0.20)
#define PROB_PAUSE   (PROB_START + 0.10)
#define PROB_FLUSH   (PROB_PAUSE + 0.10)
#define PROB_STOP    (PROB_FLUSH + 0.10)
#define PROB_CLOSE   (PROB_STOP + 0.10)
static_assert(PROB_CLOSE < 0.9, "Probability sum too high.");

aaudio_data_callback_result_t AAudioMonkeyDataCallback(
        AAudioStream *stream,
        void *userData,
        void *audioData,
        int32_t numFrames);

void AAudioMonkeyErrorCallbackProc(
        AAudioStream * /* stream */,
        void *userData,
        aaudio_result_t error);


// This function is not thread safe. Only use this from a single thread.
double nextRandomDouble() {
    return drand48();
}

class AAudioMonkey : public AAudioSimplePlayer {
public:

    AAudioMonkey(int index, AAudioArgsParser *argParser)
            : mArgParser(argParser)
            , mIndex(index) {}

    aaudio_result_t open() {
        printf("Monkey # %d ---------------------------------------------- OPEN\n", mIndex);
        double offset = mIndex * 50;
        mSine1.setup(440.0, 48000);
        mSine1.setSweep(300.0 + offset, 600.0 + offset, 5.0);
        mSine2.setup(660.0, 48000);
        mSine2.setSweep(350.0 + offset, 900.0 + offset, 7.0);

        aaudio_result_t result = AAudioSimplePlayer::open(*mArgParser,
                                      AAudioMonkeyDataCallback,
                                      AAudioMonkeyErrorCallbackProc,
                                      this);
        if (result != AAUDIO_OK) {
            printf("ERROR -  player.open() returned %d\n", result);
        }

        mArgParser->compareWithStream(getStream());
        return result;
    }

    bool isOpen() {
        return (getStream() != nullptr);

    }
    /**
     *
     * @return true if stream passes tests
     */
    bool validate() {
        if (!isOpen()) return true; // closed is OK

        // update and query stream state
        aaudio_stream_state_t state = AAUDIO_STREAM_STATE_UNKNOWN;
        aaudio_result_t result = AAudioStream_waitForStateChange(getStream(),
            AAUDIO_STREAM_STATE_UNKNOWN, &state, 0);
        if (result == AAUDIO_ERROR_DISCONNECTED) {
            printf("WARNING - AAudioStream_waitForStateChange returned DISCONNECTED\n");
            return true; // OK
        }
        if (result != AAUDIO_OK) {
            printf("ERROR - AAudioStream_waitForStateChange returned %d\n", result);
            return false;
        }

        int64_t framesRead = AAudioStream_getFramesRead(getStream());
        int64_t framesWritten = AAudioStream_getFramesWritten(getStream());
        int32_t xRuns = AAudioStream_getXRunCount(getStream());
        // Print status
        printf("%30s, framesWritten = %8lld, framesRead = %8lld, xRuns = %d\n",
               AAudio_convertStreamStateToText(state),
               (unsigned long long) framesWritten,
               (unsigned long long) framesRead,
               xRuns);

        if (state != AAUDIO_STREAM_STATE_STARTING && framesWritten < framesRead) {
            printf("WARNING - UNDERFLOW - diff = %d !!!!!!!!!!!!\n",
                   (int) (framesWritten - framesRead));
        }
        return true;
    }

    aaudio_result_t invoke() {
        aaudio_result_t result = AAUDIO_OK;
        if (!isOpen()) {
            result = open();
            if (result != AAUDIO_OK) return result;
        }

        if (!validate()) {
            return -1;
        }

        // update and query stream state
        aaudio_stream_state_t state = AAUDIO_STREAM_STATE_UNKNOWN;
        state = AAudioStream_getState(getStream());
        if (state < 0) {
            printf("ERROR - AAudioStream_getState returned %d\n", state);
            return state;
        }

        if (state == AAUDIO_STREAM_STATE_DISCONNECTED) {
            printf("#%d, Closing disconnected stream.\n", getIndex());
            result = close();
            return result;
        }

        double dice = nextRandomDouble();
        // Select an action based on a weighted probability.
        printf("    "); // indent action
        if (dice < PROB_START) {
            printf("start\n");
            result = AAudioStream_requestStart(getStream());
        } else if (dice < PROB_PAUSE) {
            printf("pause\n");
            result = AAudioStream_requestPause(getStream());
        } else if (dice < PROB_FLUSH) {
            printf("flush\n");
            result = AAudioStream_requestFlush(getStream());
        } else if (dice < PROB_STOP) {
            printf("stop\n");
            result = AAudioStream_requestStop(getStream());
        } else if (dice < PROB_CLOSE) {
            printf("close\n");
            result = close();
        } else {
            printf("do nothing\n");
        }

        if (result == AAUDIO_ERROR_INVALID_STATE) {
            printf("    got AAUDIO_ERROR_INVALID_STATE - expected from a monkey\n");
            result = AAUDIO_OK;
        }
        if (result == AAUDIO_OK && isOpen()) {
            if (!validate()) {
                result = -1;
            }
        }
        return result;
    }

    aaudio_data_callback_result_t renderAudio(
            AAudioStream *stream,
            void *audioData,
            int32_t numFrames) {

        int32_t samplesPerFrame = AAudioStream_getChannelCount(stream);
        // This code only plays on the first one or two channels.
        // TODO Support arbitrary number of channels.
        switch (AAudioStream_getFormat(stream)) {
            case AAUDIO_FORMAT_PCM_I16: {
                int16_t *audioBuffer = (int16_t *) audioData;
                // Render sine waves as shorts to first channel.
                mSine1.render(&audioBuffer[0], samplesPerFrame, numFrames);
                // Render sine waves to second channel if there is one.
                if (samplesPerFrame > 1) {
                    mSine2.render(&audioBuffer[1], samplesPerFrame, numFrames);
                }
            }
                break;
            case AAUDIO_FORMAT_PCM_FLOAT: {
                float *audioBuffer = (float *) audioData;
                // Render sine waves as floats to first channel.
                mSine1.render(&audioBuffer[0], samplesPerFrame, numFrames);
                // Render sine waves to second channel if there is one.
                if (samplesPerFrame > 1) {
                    mSine2.render(&audioBuffer[1], samplesPerFrame, numFrames);
                }
            }
                break;
            default:
                return AAUDIO_CALLBACK_RESULT_STOP;
        }
        return AAUDIO_CALLBACK_RESULT_CONTINUE;
    }

    int getIndex() const {
        return mIndex;
    }

private:
    const AAudioArgsParser  *mArgParser;
    const int                mIndex;
    SineGenerator            mSine1;
    SineGenerator            mSine2;
};

// Callback function that fills the audio output buffer.
aaudio_data_callback_result_t AAudioMonkeyDataCallback(
        AAudioStream *stream,
        void *userData,
        void *audioData,
        int32_t numFrames
) {
    // should not happen but just in case...
    if (userData == nullptr) {
        printf("ERROR - AAudioMonkeyDataCallback needs userData\n");
        return AAUDIO_CALLBACK_RESULT_STOP;
    }
    AAudioMonkey *monkey = (AAudioMonkey *) userData;
    return monkey->renderAudio(stream, audioData, numFrames);
}

void AAudioMonkeyErrorCallbackProc(
        AAudioStream * /* stream */,
        void *userData,
        aaudio_result_t error) {
    AAudioMonkey *monkey = (AAudioMonkey *) userData;
    printf("#%d, Error Callback, error: %d\n", monkey->getIndex(), (int)error);
}

static void usage() {
    AAudioArgsParser::usage();
    printf("      -i{seed}  Initial random seed\n");
    printf("      -t{count} number of monkeys in the Troop\n");
}

int main(int argc, const char **argv) {
    AAudioArgsParser argParser;
    std::vector<AAudioMonkey> monkeys;
    aaudio_result_t result;
    int numMonkeys = 1;

    // Make printf print immediately so that debug info is not stuck
    // in a buffer if we hang or crash.
    setvbuf(stdout, nullptr, _IONBF, (size_t) 0);

    printf("%s - Monkeys\n", argv[0]);

    long int seed = (long int)getNanoseconds();  // different every time by default

    for (int i = 1; i < argc; i++) {
        const char *arg = argv[i];
        if (argParser.parseArg(arg)) {
            // Handle options that are not handled by the ArgParser
            if (arg[0] == '-') {
                char option = arg[1];
                switch (option) {
                    case 'i':
                        seed = atol(&arg[2]);
                        break;
                    case 't':
                        numMonkeys = atoi(&arg[2]);
                        break;
                    default:
                        usage();
                        exit(EXIT_FAILURE);
                        break;
                }
            } else {
                usage();
                exit(EXIT_FAILURE);
                break;
            }
        }
    }

    srand48(seed);
    printf("seed = %ld, nextRandomDouble() = %f\n", seed, nextRandomDouble());

    for (int m = 0; m < numMonkeys; m++) {
        monkeys.emplace_back(m, &argParser);
    }

    for (int i = 0; i < NUM_LOOPS; i++) {
        // pick a random monkey and invoke it
        double dice = nextRandomDouble();
        int monkeyIndex = floor(dice * numMonkeys);
        printf("----------- Monkey #%d\n", monkeyIndex);
        result = monkeys[monkeyIndex].invoke();
        if (result != AAUDIO_OK) {
            goto error;
        }

        // sleep some random time
        dice = nextRandomDouble();
        dice = dice * dice * dice; // skew towards smaller delays
        int micros = (int) (dice * MAX_MICROS_DELAY);
        usleep(micros);

        // TODO consider making this multi-threaded, one thread per monkey, to catch more bugs
    }

    printf("PASS\n");
    return EXIT_SUCCESS;

error:
    printf("FAIL - AAudio result = %d = %s\n", result, AAudio_convertResultToText(result));
    usleep(1000 * 1000); // give me time to stop the logcat
    return EXIT_FAILURE;
}

