/*
 * Copyright (C) 2016, The Qt Company Ltd. All Rights Reserved.
 * Copyright (C) 2016, 2017 Konsulko Group
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#include "rtlfmradiotunercontrol.h"
#include "rtl_fm.h"

#include <QtCore/QDebug>
#include <QtCore/QTimer>
#include <QtCore/QSettings>
#include <QtMultimedia/QAudioOutput>
#include "OutputBuffer.h"

// Structure to describe FM band plans, all values in Hz.
struct fmBandPlan {
    unsigned int minFreq;
    unsigned int maxFreq;
    unsigned int freqStep;
};

// Structure to hold output context for RTL-SDR callback
struct OutputContext {
    OutputBuffer *buffer;
    bool muted;
};

class RtlFmRadioTunerControl::Private
{
public:
    Private(RtlFmRadioTunerControl *parent);
    ~Private();

    bool muted() { return mOutputContext.muted; }
    void setMuted(bool muted) { mOutputContext.muted = muted; }
    void outputStart(void);
    void outputStop(void);

private:
    RtlFmRadioTunerControl *q;

public:
    QRadioTuner::State state;
    QRadioTuner::Band band;
    int frequency;
    QRadioTuner::StereoMode stereoMode;
    int signalStrength;
    int volume;
    bool searching;
    QRadioTuner::Error error;
    QString errorString;
    QTimer timer;
    bool searchOne;
    int step;
    bool present;
    struct fmBandPlan fmBandPlan;

private:
    void outputInit(void);

    static void rtlOutputThreadFunc(int16_t *result, int result_len, void *ctx);

    QAudioOutput* mAudioOutput;
    OutputContext mOutputContext;

    QMap<QString, struct fmBandPlan> knownFmBandPlans = {
        { QString::fromUtf8("US"), { .minFreq = 87900000, .maxFreq = 107900000, .freqStep = 200000 } },
        { QString::fromUtf8("JP"), { .minFreq = 76100000, .maxFreq = 89900000, .freqStep = 100000 } },
        { QString::fromUtf8("EU"), { .minFreq = 87500000, .maxFreq = 108000000, .freqStep = 50000 } },
        { QString::fromUtf8("ITU-1"), { .minFreq = 87500000, .maxFreq = 108000000, .freqStep = 50000 } },
        { QString::fromUtf8("ITU-2"), { .minFreq = 87900000, .maxFreq = 107900000, .freqStep = 50000 } }
    };
};

RtlFmRadioTunerControl::Private::Private(RtlFmRadioTunerControl *parent)
  : q(parent),
    state(QRadioTuner::StoppedState),
    band(QRadioTuner::FM),
    stereoMode(QRadioTuner::Auto),
    signalStrength(100),
    volume(50),
    searching(false),
    error(QRadioTuner::NoError),
    searchOne(true),
    step(0)
{
    outputInit();

    // Initialize RTL-SDR dongle
    present = false;
    if(rtl_fm_init(frequency, 200000, 48000, rtlOutputThreadFunc, &mOutputContext) < 0) {
        qDebug("%s: no RTL USB adapter?", Q_FUNC_INFO);
    } else {
        present = true;
    }

    // Initialize FM band plan
    QSettings settings("AGL", "qtmultimedia-rtlfm-radio-plugin");
    if(!settings.contains("fmbandplan"))
        settings.setValue("fmbandplan", QString::fromUtf8("US"));
    if(!knownFmBandPlans.contains(settings.value("fmbandplan").toString()))
        settings.setValue("fmbandplan", QString::fromUtf8("US"));
    fmBandPlan = knownFmBandPlans.value(settings.value("fmbandplan").toString());

    // Initialize frequency to lower bound of band plan
    frequency = fmBandPlan.minFreq;

    connect(q, &RtlFmRadioTunerControl::stationFound, [this](int frequency, const QString &name) {
        qDebug() << frequency << name;
    });
    connect(q, &RtlFmRadioTunerControl::frequencyChanged, [this](int frequency) {
	// Note that the following effectively disables any attempt at seeking.
	// If seeking support is required some logic will need to be added back
	// here (and elsewhere).
	if (searching) {
            emit q->stationFound(frequency, QString("%1 MHz").arg((double) frequency / 1000000, 0, 'f', 1));
	    if (timer.isActive() && searchOne)
	        timer.stop();
	}
    });
    connect(&timer, &QTimer::timeout, [this]() {
        q->setFrequency(frequency + step);
    });
}

void RtlFmRadioTunerControl::Private::outputInit(void)
{
    // Set up the format
    QAudioFormat format;
    format.setSampleRate(24000);
    format.setChannelCount(2);
    format.setSampleSize(16);
    format.setCodec("audio/pcm");
    format.setByteOrder(QAudioFormat::LittleEndian);
    format.setSampleType(QAudioFormat::SignedInt);

    QAudioDeviceInfo info(QAudioDeviceInfo::defaultOutputDevice());
    if (!info.isFormatSupported(format)) {
        qWarning() << "Raw audio format not supported by backend, cannot play audio.";
	return;
    }

    // Create output
    mAudioOutput = new QAudioOutput(format);
    mAudioOutput->setCategory(QString("radio"));

    // Initialize context to connect RTL-SDR library to QAudioOutput,
    // including output buffer and muted state
    mOutputContext.buffer = new OutputBuffer(format, q);
    mOutputContext.muted = false;
}

void RtlFmRadioTunerControl::Private::outputStart(void)
{
    mOutputContext.buffer->start();
    mAudioOutput->start(mOutputContext.buffer);
}

void RtlFmRadioTunerControl::Private::outputStop(void)
{
    mAudioOutput->stop();
    mOutputContext.buffer->stop();
}

void RtlFmRadioTunerControl::Private::rtlOutputThreadFunc(int16_t *result, int result_len, void *ctx)
{
    const char *p = (const char *) result;
    OutputContext *context = reinterpret_cast<OutputContext*>(ctx);

    if (!context->muted) {
	context->buffer->writeData(p, result_len * 2);
    }
}

RtlFmRadioTunerControl::Private::~Private()
{
    delete mAudioOutput;
    delete mOutputContext.buffer;
    rtl_fm_cleanup();
}

RtlFmRadioTunerControl::RtlFmRadioTunerControl(QObject *parent)
  : QRadioTunerControl(parent),
    d(new Private(this))
{
}

RtlFmRadioTunerControl::~RtlFmRadioTunerControl()
{
    delete d;
}

QRadioTuner::State RtlFmRadioTunerControl::state() const
{
    return d->state;
}

QRadioTuner::Band RtlFmRadioTunerControl::band() const
{
    return d->band;
}

void RtlFmRadioTunerControl::setBand(QRadioTuner::Band band)
{
    // We only support FM, so this is a no-op, but leaving the code below
    // #if'ed out for reference.
#if 0
    if (d->band == band)
        return;
    d->band = band;
    emit bandChanged(band);
#else
    Q_UNUSED(band);
#endif
}

bool RtlFmRadioTunerControl::isBandSupported(QRadioTuner::Band band) const
{
    // We only support FM.
    switch (band) {
    case QRadioTuner::FM:
        return true;
    default:
        break;
    }
    return false;
}

int RtlFmRadioTunerControl::frequency() const
{
    return d->frequency;
}

int RtlFmRadioTunerControl::frequencyStep(QRadioTuner::Band band) const
{
    int ret = 0;

    switch (band) {
    case QRadioTuner::AM:
        ret = 1000; // 1 kHz
        break;
    case QRadioTuner::FM:
        ret = d->fmBandPlan.freqStep;
        break;
    default:
        break;
    }
    return ret;
}

QPair<int,int> RtlFmRadioTunerControl::frequencyRange(QRadioTuner::Band band) const
{
    QPair<int,int> ret;

    switch (band) {
    case QRadioTuner::AM:
        ret = qMakePair<int,int>(531000, 1602000);
        break;
    case QRadioTuner::FM:
        ret = qMakePair<int,int>(d->fmBandPlan.minFreq, d->fmBandPlan.maxFreq);
        break;
    default:
        break;
    }
    return ret;
}

void RtlFmRadioTunerControl::setFrequency(int frequency)
{
    if(!d->present)
        return;

    QPair<int,int> range = frequencyRange(d->band);
    frequency = qBound(range.first, frequency, range.second);
    if (d->frequency == frequency) {
        if (d->timer.isActive())
            d->timer.stop();
        return;
    }
    d->frequency = frequency;
    rtl_fm_set_freq(frequency);
    emit frequencyChanged(frequency);
}

bool RtlFmRadioTunerControl::isStereo() const
{
    bool ret = false;

    switch (d->stereoMode) {
    case QRadioTuner::ForceStereo:
    case QRadioTuner::Auto:
        ret = true;
        break;
    default:
        break;
    }
    return ret;
}

QRadioTuner::StereoMode RtlFmRadioTunerControl::stereoMode() const
{
    return d->stereoMode;
}

void RtlFmRadioTunerControl::setStereoMode(QRadioTuner::StereoMode stereoMode)
{
    if (d->stereoMode == stereoMode)
        return;
    bool prevStereo = isStereo();
    d->stereoMode = stereoMode;
    bool currentStereo = isStereo();
    if (prevStereo != currentStereo)
        emit stereoStatusChanged(currentStereo);
}

int RtlFmRadioTunerControl::signalStrength() const
{
    return d->signalStrength;
}

int RtlFmRadioTunerControl::volume() const
{
    return d->volume;
}

void RtlFmRadioTunerControl::setVolume(int volume)
{
    volume = qBound(0, volume, 100);
    if (d->volume == volume)
        return;
    d->volume = volume;
    emit volumeChanged(volume);
}

bool RtlFmRadioTunerControl::isMuted() const
{
    return d->muted();
}

void RtlFmRadioTunerControl::setMuted(bool muted)
{
    if (d->muted() == muted)
        return;
    d->setMuted(muted);
    emit mutedChanged(muted);
}

bool RtlFmRadioTunerControl::isSearching() const
{
    return d->searching;
}

void RtlFmRadioTunerControl::searchForward()
{
    d->searchOne = true;
    d->step = frequencyStep(d->band);
    d->timer.start(100);
    setSearching(true);
}

void RtlFmRadioTunerControl::searchBackward()
{
    d->searchOne = true;
    d->step = -frequencyStep(d->band);
    d->timer.start(250);
    setSearching(true);
}

void RtlFmRadioTunerControl::searchAllStations(QRadioTuner::SearchMode searchMode)
{
    Q_UNUSED(searchMode)
    d->searchOne = false;
    d->frequency = frequencyRange(d->band).first;
    d->step = frequencyStep(d->band);
    d->timer.start(250);
    setSearching(true);
}

void RtlFmRadioTunerControl::cancelSearch()
{
    d->timer.stop();
    setSearching(false);
}

void RtlFmRadioTunerControl::setSearching(bool searching)
{
    if (d->searching == searching)
        return;
    d->searching = searching;
    emit searchingChanged(searching);
}

void RtlFmRadioTunerControl::start()
{
    if(!d->present)
        return;

    qDebug() << "RtlFmRadioTunerControl::start - enter\n";
    if(d->state == QRadioTuner::ActiveState) {
        qDebug() << "RtlFmRadioTunerControl::start - already running, exit\n";
        return;
    }

    //rtl_fm_stop();
    rtl_fm_start();
    d->outputStart();
    d->state = QRadioTuner::ActiveState;
    emit stateChanged(d->state);
    qDebug() << "RtlFmRadioTunerControl::start - exit\n";
}

void RtlFmRadioTunerControl::stop()
{
    if(!d->present)
        return;

    qDebug() << "RtlFmRadioTunerControl::stop - enter\n";
    if(d->state == QRadioTuner::StoppedState) {
        qDebug() << "RtlFmRadioTunerControl::stop - already stopped, exit\n";
        return;
    }

    rtl_fm_stop();
    d->outputStop();
    d->state = QRadioTuner::StoppedState;
    emit stateChanged(d->state);
    qDebug() << "RtlFmRadioTunerControl::stop - exit\n";
}

QRadioTuner::Error RtlFmRadioTunerControl::error() const
{
    return d->error;
}

QString RtlFmRadioTunerControl::errorString() const
{
    return d->errorString;
}

void RtlFmRadioTunerControl::setError(QRadioTuner::Error error, const QString &errorString)
{
    d->error = error;
    d->errorString = errorString;
    emit QRadioTunerControl::error(error);
}