/***************************************************************************
              SonagramPlugin.cpp  -  plugin that shows a sonagram window
                             -------------------
    begin                : Fri Jul 28 2000
    copyright            : (C) 2000 by Thomas Eschenbacher
    email                : Thomas.Eschenbacher@gmx.de
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

#include "config.h"

#include <errno.h>
#include <math.h>
#include <stdlib.h>

#include <limits>
#include <new>

#include <QApplication>
#include <QColor>
#include <QFutureSynchronizer>
#include <QImage>
#include <QMutexLocker>
#include <QPointer>
#include <QString>
#include <QtConcurrentRun>

#include "libkwave/GlobalLock.h"
#include "libkwave/MessageBox.h"
#include "libkwave/MultiTrackReader.h"
#include "libkwave/Plugin.h"
#include "libkwave/PluginManager.h"
#include "libkwave/Sample.h"
#include "libkwave/SampleReader.h"
#include "libkwave/SignalManager.h"
#include "libkwave/Track.h"
#include "libkwave/Utils.h"
#include "libkwave/WindowFunction.h"

#include "libgui/OverViewCache.h"
#include "libgui/SelectionTracker.h"

#include "SonagramDialog.h"
#include "SonagramPlugin.h"
#include "SonagramWindow.h"

KWAVE_PLUGIN(sonagram, SonagramPlugin)

/**
 * interval for limiting the number of repaints per second [ms]
 */
#define REPAINT_INTERVAL 250

//***************************************************************************
Kwave::SonagramPlugin::SonagramPlugin(QObject *parent,
                                      const QVariantList &args)
    :Kwave::Plugin(parent, args),
     m_sonagram_window(nullptr),
     m_selection(nullptr),
     m_slices(0), m_fft_points(0),
     m_window_type(Kwave::WINDOW_FUNC_NONE), m_color(true),
     m_track_changes(true), m_follow_selection(false), m_image(),
     m_overview_cache(nullptr), m_slice_pool(), m_valid(MAX_SLICES, false),
     m_pending_jobs(), m_lock_job_list(), m_future(),
     m_repaint_timer()
{
    i18n("Sonagram");

    // connect the output ouf the sonagram worker thread
    connect(this, SIGNAL(sliceAvailable(Kwave::SonagramPlugin::Slice*)),
            this, SLOT(insertSlice(Kwave::SonagramPlugin::Slice*)),
            Qt::QueuedConnection);

    // connect repaint timer
    connect(&m_repaint_timer, SIGNAL(timeout()),
            this, SLOT(validate()));
}

//***************************************************************************
Kwave::SonagramPlugin::~SonagramPlugin()
{
    m_repaint_timer.stop();

    delete m_sonagram_window;
    m_sonagram_window = nullptr;

    delete m_selection;
    m_selection = nullptr;
}

//***************************************************************************
QStringList *Kwave::SonagramPlugin::setup(QStringList &previous_params)
{
    QStringList *result = nullptr;

    // try to interpret the list of previous parameters, ignore errors
    if (previous_params.count()) interpreteParameters(previous_params);

    QPointer<Kwave::SonagramDialog> dlg =
        new(std::nothrow) Kwave::SonagramDialog(*this);
    Q_ASSERT(dlg);
    if (!dlg) return nullptr;

    dlg->setWindowFunction(m_window_type);
    dlg->setColorMode(m_color ? 1 : 0);
    dlg->setTrackChanges(m_track_changes);
    dlg->setFollowSelection(m_follow_selection);

    if ((dlg->exec() == QDialog::Accepted) && dlg) {
        result = new(std::nothrow) QStringList();
        Q_ASSERT(result);
        if (result) dlg->parameters(*result);
    }

    delete dlg;
    return result;
}

//***************************************************************************
int Kwave::SonagramPlugin::interpreteParameters(QStringList &params)
{
    bool ok;
    QString param;

    // evaluate the parameter list
    if (params.count() != 5) return -EINVAL;

    param = params[0];
    m_fft_points = param.toUInt(&ok);
    if (!ok) return -EINVAL;
    if (m_fft_points > MAX_FFT_POINTS) m_fft_points = MAX_FFT_POINTS;

    param = params[1];
    m_window_type = Kwave::WindowFunction::findFromName(param);

    param = params[2];
    m_color = (param.toUInt(&ok) != 0);
    if (!ok) return -EINVAL;

    param = params[3];
    m_track_changes = (param.toUInt(&ok) != 0);
    if (!ok) return -EINVAL;

    param = params[4];
    m_follow_selection = (param.toUInt(&ok) != 0);
    if (!ok) return -EINVAL;

    return 0;
}

//***************************************************************************
int Kwave::SonagramPlugin::start(QStringList &params)
{
    // clean up leftovers from last run
    delete m_sonagram_window;
    m_sonagram_window = nullptr;
    delete m_selection;
    m_selection = nullptr;
    delete m_overview_cache;
    m_overview_cache = nullptr;

    Kwave::SignalManager &sig_mgr = signalManager();

    // interpret parameter list and abort if it contains invalid data
    int result = interpreteParameters(params);
    if (result) return result;

    // create an empty sonagram window
    m_sonagram_window = new(std::nothrow)
        Kwave::SonagramWindow(parentWidget(), signalName());
    Q_ASSERT(m_sonagram_window);
    if (!m_sonagram_window) return -ENOMEM;

    // if the signal closes, close the sonagram window too
    QObject::connect(&manager(), SIGNAL(sigClosed()),
                     m_sonagram_window, SLOT(close()));

    // get the current selection
    QVector<unsigned int> selected_channels;
    sample_index_t offset = 0;
    sample_index_t length = 0;
    length = selection(&selected_channels, &offset, nullptr, true);

    // abort if nothing is selected
    if (!length || selected_channels.isEmpty())
        return -EINVAL;

    // calculate the number of slices (width of image)
    m_slices = Kwave::toUint(ceil(static_cast<double>(length) /
                                  static_cast<double>(m_fft_points)));
    if (m_slices > MAX_SLICES) m_slices = MAX_SLICES;

    /* limit selection to INT_MAX samples (limitation of the cache index) */
    if ((length / m_fft_points) >= SAMPLE_INDEX_MAX) {
        Kwave::MessageBox::error(parentWidget(),
                                 i18n("File or selection too large"));
        return -EFBIG;
    }

    // create a selection tracker
    m_selection = new(std::nothrow) Kwave::SelectionTracker(
        &sig_mgr, offset, length, &selected_channels);
    Q_ASSERT(m_selection);
    if (!m_selection) return -ENOMEM;

    connect(m_selection, SIGNAL(sigTrackInserted(QUuid)),
            this,        SLOT(slotTrackInserted(QUuid)));
    connect(m_selection, SIGNAL(sigTrackDeleted(QUuid)),
            this,        SLOT(slotTrackDeleted(QUuid)));
    connect(
        m_selection,
        SIGNAL(sigInvalidated(const QUuid*,sample_index_t,sample_index_t)),
        this,
        SLOT(slotInvalidated(const QUuid*,sample_index_t,sample_index_t))
    );

    // create a new empty image
    createNewImage(m_slices, m_fft_points / 2);

    // set the overview
    m_overview_cache = new(std::nothrow)
        Kwave::OverViewCache(sig_mgr, offset, length, &selected_channels);
    Q_ASSERT(m_overview_cache);
    if (!m_overview_cache) return -ENOMEM;

    refreshOverview(); // <- this needs the m_overview_cache

    if (m_track_changes) {
        // stay informed about changes in the signal
        connect(m_overview_cache, SIGNAL(changed()),
                this, SLOT(refreshOverview()));
    } else {
        // overview cache is no longer needed
        delete m_overview_cache;
        m_overview_cache = nullptr;
    }

    // connect all needed signals
    connect(m_sonagram_window, SIGNAL(destroyed()),
            this, SLOT(windowDestroyed()));

    // activate the window with an initial image
    // and all necessary information
    m_sonagram_window->setColorMode((m_color) ? 1 : 0);
    m_sonagram_window->setImage(m_image);
    m_sonagram_window->setPoints(m_fft_points);
    m_sonagram_window->setRate(signalRate());
    m_sonagram_window->show();

    if (m_track_changes) {
        QObject::connect(static_cast<QObject*>(&(manager())),
            SIGNAL(sigSignalNameChanged(QString)),
            m_sonagram_window, SLOT(setName(QString)));
    }

    // increment the usage counter and release the plugin when the
    // sonagram window closed
    use();

    return 0;
}

//***************************************************************************
void Kwave::SonagramPlugin::makeAllValid()
{
    unsigned int             fft_points;
    unsigned int             slices;
    Kwave::window_function_t window_type;
    sample_index_t           first_sample;
    sample_index_t           last_sample;
    QBitArray                valid;
    QVector<unsigned int>    track_list;

    {
        QMutexLocker _lock(&m_lock_job_list);

        if (!m_selection) return;
        if (!m_selection->length() || (m_fft_points < 4)) return;

        fft_points   = m_fft_points;
        slices       = m_slices;
        window_type  = m_window_type;
        first_sample = m_selection->first();
        last_sample  = m_selection->last();
        valid        = m_valid;
        m_valid.fill(true);

        const QList<QUuid> selected_tracks(m_selection->allTracks());
        for (unsigned int track : signalManager().allTracks())
            if (selected_tracks.contains(signalManager().uuidOfTrack(track)))
                track_list.append(track);
    }
    const unsigned int tracks = static_cast<unsigned int>(track_list.count());

    Kwave::WindowFunction func(window_type);
    const QVector<double> windowfunction = func.points(fft_points);
    Q_ASSERT(windowfunction.count() == Kwave::toInt(fft_points));
    if (windowfunction.count() != Kwave::toInt(fft_points)) return;

    Kwave::MultiTrackReader source(Kwave::SinglePassForward,
        signalManager(), track_list, first_sample, last_sample);

//     qDebug("SonagramPlugin[%p]::makeAllValid() [%llu .. %llu]",
//      static_cast<void *>(this), first_sample, last_sample);

    QFutureSynchronizer<void> synchronizer;
    for (unsigned int slice_nr = 0; slice_nr < slices; slice_nr++) {
//      qDebug("SonagramPlugin::run(): calculating slice %d of %d",
//             slice_nr, m_slices);

        if (valid[slice_nr]) continue;

        // determine start of the stripe
        sample_index_t pos = first_sample + (slice_nr * fft_points);

        // get a new slice from the pool and initialize it
        Kwave::SonagramPlugin::Slice *slice = m_slice_pool.allocate();
        Q_ASSERT(slice);

        slice->m_index = slice_nr;
        memset(slice->m_input,  0x00, sizeof(slice->m_input));
        memset(slice->m_output, 0x00, sizeof(slice->m_output));

        if ((pos <= last_sample) && (tracks)) {
            // initialize result with zeroes
            memset(slice->m_result, 0x00, sizeof(slice->m_result));

            // seek to the start of the slice
            source.seek(pos);

            // we have a new slice, now fill it's input buffer
            double *in = slice->m_input;
            for (unsigned int j = 0; j < fft_points; j++) {
                double value = 0.0;
                if (!(source.eof())) {
                    for (unsigned int t = 0; t < tracks; t++) {
                        sample_t s = 0;
                        Kwave::SampleReader *reader = source[t];
                        Q_ASSERT(reader);
                        if (reader) *reader >> s;
                        value += sample2double(s);
                    }
                    value /= tracks;
                }
                in[j] = value * windowfunction[j];
            }

            // a background job is running soon
            // (for counterpart, see insertSlice(...) below [main thread])
            m_pending_jobs.lockForRead();

            // run the FFT in a background thread
            synchronizer.addFuture(QtConcurrent::run(
                &Kwave::SonagramPlugin::calculateSlice, this, slice)
            );
        } else {
            // range has been deleted -> fill with "empty"
            memset(slice->m_result, 0xFF, sizeof(slice->m_result));
            m_pending_jobs.lockForRead();
            emit sliceAvailable(slice);
        }

        if (shouldStop()) break;
    }

//     qDebug("SonagramPlugin::makeAllValid(): waiting for background jobs...");

    // wait for all worker threads
    synchronizer.waitForFinished();

    // wait for queued signals
    m_pending_jobs.lockForWrite();
    m_pending_jobs.unlock();

//     qDebug("SonagramPlugin::makeAllValid(): done.");
}

//***************************************************************************
void Kwave::SonagramPlugin::run(QStringList params)
{
    qDebug("SonagramPlugin::run()");
    Q_UNUSED(params)
    {
        // invalidate all slices
        QMutexLocker _lock(&m_lock_job_list);
        m_valid.fill(false);
    }
    makeAllValid();
}

//***************************************************************************
void Kwave::SonagramPlugin::calculateSlice(Kwave::SonagramPlugin::Slice *slice)
{
    fftw_plan p;

    // prepare for a 1-dimensional real-to-complex DFT
    {
        Kwave::GlobalLock _lock; // libfftw is not threadsafe!
        p = fftw_plan_dft_r2c_1d(
            m_fft_points,
            &(slice->m_input[0]),
            &(slice->m_output[0]),
            FFTW_ESTIMATE
        );
    }
    Q_ASSERT(p);
    if (!p) return;

    // calculate the fft (according to the specs, this is the one and only
    // libfft function that is threadsafe!)
    fftw_execute(p);

    // norm all values to [0...254] and use them as pixel value
    const double scale = static_cast<double>(m_fft_points) / 254.0;
    for (unsigned int j = 0; j < m_fft_points / 2; j++) {
        // get signal energy and scale to [0 .. 254]
        double rea = slice->m_output[j][0];
        double ima = slice->m_output[j][1];
        double a = ((rea * rea) + (ima * ima)) / scale;

        slice->m_result[j] = static_cast<unsigned char>(qMin(a, double(254.0)));
    }

    // free the allocated FFT resources
    {
        Kwave::GlobalLock _lock; // libfftw is not threadsafe!
        fftw_destroy_plan(p);
    }

    // emit the slice data to be synchronously inserted into
    // the current image in the context of the main thread
    // (Qt does the queuing for us)
    emit sliceAvailable(slice);
}

//***************************************************************************
void Kwave::SonagramPlugin::insertSlice(Kwave::SonagramPlugin::Slice *slice)
{
    // check: this must be called from the GUI thread only!
    Q_ASSERT(this->thread() == QThread::currentThread());
    Q_ASSERT(this->thread() == qApp->thread());

    Q_ASSERT(slice);
    if (!slice) return;

    QByteArray result;
    result.setRawData(reinterpret_cast<char *>(&(slice->m_result[0])),
                      m_fft_points / 2);
    unsigned int nr = slice->m_index;

    // forward the slice to the window to display it
    if (m_sonagram_window) m_sonagram_window->insertSlice(nr, result);

    // return the slice into the pool
    m_slice_pool.release(slice);

    // job is done
    m_pending_jobs.unlock();
}

//***************************************************************************
void Kwave::SonagramPlugin::createNewImage(const unsigned int width,
                                           const unsigned int height)
{
    // delete the previous image
    m_image = QImage();
    if (m_sonagram_window) m_sonagram_window->setImage(m_image);

    // do not create a new image if one dimension is zero!
    Q_ASSERT(width);
    Q_ASSERT(height);
    if (!width || !height) return;

    // also do not create if the image size is out of range
    Q_ASSERT(width <= 32767);
    Q_ASSERT(height <= 32767);
    if ((width >= 32767) || (height >= 32767)) return;

    // create the new image object
    m_image = QImage(width, height, QImage::Format_Indexed8);
    Q_ASSERT(!m_image.isNull());
    if (m_image.isNull()) return;

    // initialize the image's palette with transparency
    m_image.setColorCount(256);
    for (int i = 0; i < 256; i++) {
        m_image.setColor(i, 0x00000000);
    }

    // fill the image with "empty" (transparent)
    m_image.fill(0xFF);
}

//***************************************************************************
void Kwave::SonagramPlugin::refreshOverview()
{
    if (!m_overview_cache || !m_sonagram_window) return;

    QColor fg = m_sonagram_window->palette().light().color();
    QColor bg = m_sonagram_window->palette().mid().color();
    QImage overview = m_overview_cache->getOverView(
       m_sonagram_window->width(), SONAGRAM_OVERVIEW_HEIGHT, fg, bg);

    m_sonagram_window->setOverView(overview);
}

//***************************************************************************
void Kwave::SonagramPlugin::requestValidation()
{
    // only re-start the repaint timer, this hides some GUI update artifacts
    if (!m_repaint_timer.isActive()) {
        m_repaint_timer.stop();
        m_repaint_timer.setSingleShot(true);
        m_repaint_timer.start(REPAINT_INTERVAL);
    }
}

//***************************************************************************
void Kwave::SonagramPlugin::validate()
{
    // wait for previously running jobs to finish
    if (m_future.isRunning()) {
        requestValidation();
        return; // job is still running, come back later...
    }

    // queue a background thread for updates
    m_future = QtConcurrent::run(&Kwave::SonagramPlugin::makeAllValid, this);
}

//***************************************************************************
void Kwave::SonagramPlugin::slotTrackInserted(const QUuid &track_id)
{
    QMutexLocker _lock(&m_lock_job_list);

    Q_UNUSED(track_id)

    // check for "track changes" mode
    if (!m_track_changes) return;

    // invalidate complete signal
    m_valid.fill(false, m_slices);
    requestValidation();
}

//***************************************************************************
void Kwave::SonagramPlugin::slotTrackDeleted(const QUuid &track_id)
{
    QMutexLocker _lock(&m_lock_job_list);

    Q_UNUSED(track_id)

    // check for "track changes" mode
    if (!m_track_changes) return;

    // invalidate complete signal
    m_valid.fill(false, m_slices);
    requestValidation();
}

//***************************************************************************
void Kwave::SonagramPlugin::slotInvalidated(const QUuid *track_id,
                                            sample_index_t first,
                                            sample_index_t last)
{
    QMutexLocker lock(&m_lock_job_list);

    Q_UNUSED(track_id)
//     qDebug("SonagramPlugin[%p]::slotInvalidated(%s, %llu, %llu)",
//          static_cast<void *>(this),
//         (track_id) ? DBG(track_id->toString()) : "*", first, last);

    // check for "track changes" mode
    if (!m_track_changes) return;

    // adjust offsets, absolute -> relative
    sample_index_t offset = (m_selection) ? m_selection->offset() : 0;
    Q_ASSERT(first >= offset);
    Q_ASSERT(last  >= offset);
    Q_ASSERT(last  >= first);
    first -= offset;
    last  -= offset;

    unsigned int first_idx = Kwave::toUint(first / m_fft_points);
    unsigned int last_idx;
    if (last >= (SAMPLE_INDEX_MAX - (m_fft_points - 1)))
        last_idx = m_slices - 1;
    else
        last_idx = Kwave::toUint(qMin(Kwave::round_up(last,
            static_cast<sample_index_t>(m_fft_points)) / m_fft_points,
            static_cast<sample_index_t>(m_slices - 1))
        );

    m_valid.fill(false, first_idx, last_idx + 1);
    requestValidation();
}

//***************************************************************************
void Kwave::SonagramPlugin::windowDestroyed()
{
    cancel();

    m_sonagram_window = nullptr; // closes itself !

    delete m_selection;
    m_selection = nullptr;

    delete m_overview_cache;
    m_overview_cache = nullptr;

    release();
}

//***************************************************************************
#include "SonagramPlugin.moc"
//***************************************************************************
//***************************************************************************

#include "moc_SonagramPlugin.cpp"
