/*
 * Copyright (C) 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_NDEBUG 0
#define LOG_TAG "MidiExtractor"
#include <utils/Log.h>

#include "MidiExtractor.h"

#include <com_android_media_extractor_flags.h>
#include <media/MidiIoWrapper.h>
#include <media/stagefright/foundation/ADebug.h>
#include <media/stagefright/MediaBufferGroup.h>
#include <media/stagefright/MediaDefs.h>
#include <media/stagefright/MediaErrors.h>
#include <libsonivox/eas_reverb.h>
#include <watchdog/Watchdog.h>

namespace android {

// how many Sonivox output buffers to aggregate into one MediaBuffer
static const int NUM_COMBINE_BUFFERS = 4;

class MidiSource : public MediaTrackHelper {

public:
    MidiSource(
            MidiEngine &engine,
            AMediaFormat *trackMetadata);

    virtual media_status_t start();
    virtual media_status_t stop();
    virtual media_status_t getFormat(AMediaFormat *);

    virtual media_status_t read(
            MediaBufferHelper **buffer, const ReadOptions *options = NULL);

protected:
    virtual ~MidiSource();

private:
    MidiEngine &mEngine;
    AMediaFormat *mTrackMetadata;
    bool mInitCheck;
    bool mStarted;

    status_t init();

    // no copy constructor or assignment
    MidiSource(const MidiSource &);
    MidiSource &operator=(const MidiSource &);

};


// Midisource

MidiSource::MidiSource(
        MidiEngine &engine,
        AMediaFormat *trackMetadata)
    : mEngine(engine),
      mTrackMetadata(trackMetadata),
      mInitCheck(false),
      mStarted(false)
{
    ALOGV("MidiSource ctor");
    mInitCheck = init();
}

MidiSource::~MidiSource()
{
    ALOGV("MidiSource dtor");
    if (mStarted) {
        stop();
    }
}

media_status_t MidiSource::start()
{
    ALOGV("MidiSource::start");

    CHECK(!mStarted);
    mStarted = true;
    mEngine.allocateBuffers(mBufferGroup);
    return AMEDIA_OK;
}

media_status_t MidiSource::stop()
{
    ALOGV("MidiSource::stop");

    CHECK(mStarted);
    mStarted = false;
    mEngine.releaseBuffers();

    return AMEDIA_OK;
}

media_status_t MidiSource::getFormat(AMediaFormat *meta)
{
    return AMediaFormat_copy(meta, mTrackMetadata);
}

media_status_t MidiSource::read(
        MediaBufferHelper **outBuffer, const ReadOptions *options)
{
    ALOGV("MidiSource::read");

    MediaBufferHelper *buffer;
    // process an optional seek request
    int64_t seekTimeUs;
    ReadOptions::SeekMode mode;
    if ((NULL != options) && options->getSeekTo(&seekTimeUs, &mode)) {
        if (seekTimeUs <= 0LL) {
            seekTimeUs = 0LL;
        }
        mEngine.seekTo(seekTimeUs);
    }
    buffer = mEngine.readBuffer();
    *outBuffer = buffer;
    ALOGV("MidiSource::read %p done", this);
    return buffer != NULL ? AMEDIA_OK : AMEDIA_ERROR_END_OF_STREAM;
}

status_t MidiSource::init()
{
    ALOGV("MidiSource::init");
    return OK;
}

// MidiEngine
using namespace std::chrono_literals;
static constexpr auto kTimeout = 10s;

MidiEngine::MidiEngine(CDataSource *dataSource,
        AMediaFormat *fileMetadata,
        AMediaFormat *trackMetadata) :
            mEasData(NULL),
            mEasHandle(NULL),
            mEasConfig(NULL),
            mIsInitialized(false) {
    Watchdog watchdog(kTimeout);

    mIoWrapper = new MidiIoWrapper(dataSource);
    // spin up a new EAS engine
    EAS_I32 temp;
    EAS_RESULT result = EAS_Init(&mEasData);

    if (result == EAS_SUCCESS) {
        result = EAS_OpenFile(mEasData, mIoWrapper->getLocator(), &mEasHandle);
    }
    if (result == EAS_SUCCESS) {
        result = EAS_Prepare(mEasData, mEasHandle);
    }
    if (result == EAS_SUCCESS) {
        result = EAS_ParseMetaData(mEasData, mEasHandle, &temp);
    }

    if (result != EAS_SUCCESS) {
        return;
    }

    if (fileMetadata != NULL) {
        AMediaFormat_setString(fileMetadata, AMEDIAFORMAT_KEY_MIME, MEDIA_MIMETYPE_AUDIO_MIDI);
    }

    if (trackMetadata != NULL) {
        AMediaFormat_setString(trackMetadata, AMEDIAFORMAT_KEY_MIME, MEDIA_MIMETYPE_AUDIO_RAW);
        AMediaFormat_setInt64(
                trackMetadata, AMEDIAFORMAT_KEY_DURATION, 1000ll * temp); // milli->micro
        mEasConfig = EAS_Config();
        AMediaFormat_setInt32(
                trackMetadata, AMEDIAFORMAT_KEY_SAMPLE_RATE, mEasConfig->sampleRate);
        AMediaFormat_setInt32(
                trackMetadata, AMEDIAFORMAT_KEY_CHANNEL_COUNT, mEasConfig->numChannels);
        AMediaFormat_setInt32(
                trackMetadata, AMEDIAFORMAT_KEY_PCM_ENCODING, kAudioEncodingPcm16bit);
    }
    mIsInitialized = true;
}

MidiEngine::~MidiEngine() {
    Watchdog watchdog(kTimeout);

    if (mEasHandle) {
        EAS_CloseFile(mEasData, mEasHandle);
    }
    if (mEasData) {
        EAS_Shutdown(mEasData);
    }
    delete mIoWrapper;
}

status_t MidiEngine::initCheck() {
    return mIsInitialized ? OK : UNKNOWN_ERROR;
}

status_t MidiEngine::allocateBuffers(MediaBufferGroupHelper *group) {
    // select reverb preset and enable
    EAS_SetParameter(mEasData, EAS_MODULE_REVERB, EAS_PARAM_REVERB_PRESET, EAS_PARAM_REVERB_CHAMBER);
    EAS_SetParameter(mEasData, EAS_MODULE_REVERB, EAS_PARAM_REVERB_BYPASS, EAS_FALSE);

    int bufsize = sizeof(EAS_PCM)
            * mEasConfig->mixBufferSize * mEasConfig->numChannels * NUM_COMBINE_BUFFERS;
    ALOGV("using %d byte buffer", bufsize);
    mGroup = group;
    mGroup->add_buffer(bufsize);
    return OK;
}

status_t MidiEngine::releaseBuffers() {
    return OK;
}

status_t MidiEngine::seekTo(int64_t positionUs) {
    Watchdog watchdog(kTimeout);

    ALOGV("seekTo %lld", (long long)positionUs);
    EAS_RESULT result = EAS_Locate(mEasData, mEasHandle, positionUs / 1000, false);
    return result == EAS_SUCCESS ? OK : UNKNOWN_ERROR;
}

MediaBufferHelper* MidiEngine::readBuffer() {
    Watchdog watchdog(kTimeout);

    EAS_STATE state;
    EAS_State(mEasData, mEasHandle, &state);
    if ((state == EAS_STATE_STOPPED) || (state == EAS_STATE_ERROR)) {
        return NULL;
    }
    MediaBufferHelper *buffer = nullptr;
    status_t err = mGroup->acquire_buffer(&buffer);
    if (err != OK || buffer == nullptr) {
        ALOGE("readBuffer: no buffer");
        return NULL;
    }
    EAS_I32 timeMs;
    EAS_GetLocation(mEasData, mEasHandle, &timeMs);
    int64_t timeUs = 1000ll * timeMs;
    AMediaFormat *meta = buffer->meta_data();
    AMediaFormat_setInt64(meta, AMEDIAFORMAT_KEY_TIME_US, timeUs);
    AMediaFormat_setInt32(meta, AMEDIAFORMAT_KEY_IS_SYNC_FRAME, 1);

    EAS_PCM* p = (EAS_PCM*) buffer->data();
    int numBytesOutput = 0;
    for (int i = 0; i < NUM_COMBINE_BUFFERS; i++) {
        EAS_I32 numRendered;
        EAS_RESULT result = EAS_Render(mEasData, p, mEasConfig->mixBufferSize, &numRendered);
        if (result != EAS_SUCCESS) {
            ALOGE("EAS_Render() returned %ld, numBytesOutput = %d", result, numBytesOutput);
            buffer->release();
            return NULL; // Stop processing to prevent infinite loops.
        }
        p += numRendered * mEasConfig->numChannels;
        numBytesOutput += numRendered * mEasConfig->numChannels * sizeof(EAS_PCM);
    }
    buffer->set_range(0, numBytesOutput);
    ALOGV("readBuffer: returning %zd in buffer %p", buffer->range_length(), buffer);
    return buffer;
}


// MidiExtractor

MidiExtractor::MidiExtractor(
        CDataSource *dataSource)
    : mDataSource(dataSource),
      mInitCheck(false)
{
    ALOGV("MidiExtractor ctor");
    mFileMetadata = AMediaFormat_new();
    mTrackMetadata = AMediaFormat_new();
    mEngine = new MidiEngine(mDataSource, mFileMetadata, mTrackMetadata);
    mInitCheck = mEngine->initCheck();
}

MidiExtractor::~MidiExtractor()
{
    ALOGV("MidiExtractor dtor");
    AMediaFormat_delete(mFileMetadata);
    AMediaFormat_delete(mTrackMetadata);
    delete mEngine;
}

size_t MidiExtractor::countTracks()
{
    return mInitCheck == OK ? 1 : 0;
}

MediaTrackHelper *MidiExtractor::getTrack(size_t index)
{
    if (mInitCheck != OK || index > 0) {
        return NULL;
    }
    return new MidiSource(*mEngine, mTrackMetadata);
}

media_status_t MidiExtractor::getTrackMetaData(
        AMediaFormat *meta,
        size_t index, uint32_t /* flags */) {
    ALOGV("MidiExtractor::getTrackMetaData");
    if (mInitCheck != OK || index > 0) {
        return AMEDIA_ERROR_UNKNOWN;
    }
    return AMediaFormat_copy(meta, mTrackMetadata);
}

media_status_t MidiExtractor::getMetaData(AMediaFormat *meta)
{
    ALOGV("MidiExtractor::getMetaData");
    return AMediaFormat_copy(meta, mFileMetadata);
}

static bool startsWith(const uint8_t *buf, size_t size, const char *pattern, size_t patternSize) {
    if (size < patternSize) {
        return false;
    }
    return (memcmp(buf, pattern, patternSize) == 0);
}

static bool isValidMThd(const uint8_t *buf, size_t size) {
    return startsWith(buf, size, "MThd", 4);
}

static bool isValidXmf(const uint8_t *buf, size_t size) {
    return startsWith(buf, size, "XMF_", 4);
}

static bool isValidImelody(const uint8_t *buf, size_t size) {
    return startsWith(buf, size, "BEGIN:IMELODY", 13);
}

static bool isValidRtttl(const uint8_t *buf, size_t size) {
    #define RTTTL_MAX_TITLE_LEN 32
    // rtttl starts with the following:
    // <title>:<type>=<value>
    //
    // Where:
    // - <title>: Up to 32 characters
    // - <type>: Single character indicating:
    //     'd' for duration
    //     'o' for octave
    //     'b' for beats per minute
    // - <value>: Corresponding value for the type
    if (size < 4) {
        return false;
    }
    for (size_t i = 0; i < RTTTL_MAX_TITLE_LEN && i < size; i++) {
        if (buf[i] == ':') {
            if (i < (size - 3) && buf[i + 2] == '=') {
                return true;
            }
            break;
        }
    }
    return false;
}

static bool isValidOta(const uint8_t *buf, size_t size) {
    #define OTA_RINGTONE 0x25
    #define OTA_SOUND 0x1d
    #define OTA_UNICODE 0x22

    // ota starts with the following:
    // <cmdLen><cmd1><cmd2>..<cmdN>
    //
    // Where:
    // - <cmdLen>: Single character command length
    // - <cmd1>: Single character (OTA_RINGTONE << 1)
    // - <cmd2>: Single character (OTA_SOUND << 1) or (OTA_UNICODE << 1)
    //           and so on with last cmd being (0x1d << 1)

    if (size < 3) {
        return false;
    }

    uint8_t cmdLen = buf[0];
    if (cmdLen < 2) {
        return false;
    }

    if ((buf[1] >> 1) != OTA_RINGTONE) {
        return false;
    }
    cmdLen--;

    size_t i = 2;
    while(cmdLen && i < size) {
        switch(buf[i] >> 1) {
            case OTA_SOUND:
                return true;
            case OTA_UNICODE:
                break;
            default:
                return false;
        }
        cmdLen--;
        i++;
    }

    return false;
}

bool SniffMidiLegacy(CDataSource *source, float *confidence) {
    MidiEngine p(source, NULL, NULL);
    if (p.initCheck() == OK) {
        *confidence = 0.8;
        ALOGV("SniffMidi: yes");
        return true;
    }
    ALOGV("SniffMidi: no");
    return false;
}

bool SniffMidiEfficiently(CDataSource *source, float *confidence) {
    uint8_t header[128];
    int filled = source->readAt(source->handle, 0, header, sizeof(header));

    if (isValidMThd(header, filled)) {
        *confidence = 0.80;
        ALOGV("SniffMidi: yes, MThd");
        return true;
    }
    if (isValidXmf(header, filled)) {
        *confidence = 0.80;
        ALOGV("SniffMidi: yes, XMF_");
        return true;
    }
    if (isValidImelody(header, filled)) {
        *confidence = 0.80;
        ALOGV("SniffMidi: yes, imelody");
        return true;
    }
    if (isValidRtttl(header, filled)) {
        *confidence = 0.80;
        ALOGV("SniffMidi: yes, rtttl");
        return true;
    }
    if (isValidOta(header, filled)) {
        *confidence = 0.80;
        ALOGV("SniffMidi: yes, ota");
        return true;
    }
    ALOGV("SniffMidi: no");
    return false;
}

// Sniffer
bool SniffMidi(CDataSource *source, float *confidence) {
    if(com::android::media::extractor::flags::extractor_sniff_midi_optimizations()) {
        return SniffMidiEfficiently(source, confidence);
    }
    return SniffMidiLegacy(source, confidence);
}

static const char *extensions[] = {
    "imy",
    "mid",
    "midi",
    "mxmf",
    "ota",
    "rtttl",
    "rtx",
    "smf",
    "xmf",
    NULL
};

extern "C" {
// This is the only symbol that needs to be exported
__attribute__ ((visibility ("default")))
ExtractorDef GETEXTRACTORDEF() {
    return {
        EXTRACTORDEF_VERSION,
        UUID("ef6cca0a-f8a2-43e6-ba5f-dfcd7c9a7ef2"),
        1,
        "MIDI Extractor",
        {
            .v3 = {
                [](
                CDataSource *source,
                float *confidence,
                void **,
                FreeMetaFunc *) -> CreatorFunc {
                    if (SniffMidi(source, confidence)) {
                        return [](
                                CDataSource *source,
                                void *) -> CMediaExtractor* {
                            return wrap(new MidiExtractor(source));};
                    }
                    return NULL;
                },
                extensions
            }
        },
    };
}

} // extern "C"

}  // namespace android
