/*
    SPDX-FileCopyrightText: 2012 Simon Andreas Eugster <simon.eu@gmail.com>
    This file is part of kdenlive. See www.kdenlive.org.

SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/

#include "audioEnvelope.h"
#include "audioStreamInfo.h"
#include "bin/bin.h"
#include "bin/projectclip.h"
#include "core.h"
#include "kdenlive_debug.h"
#include <KLocalizedString>
#include <QElapsedTimer>
#include <QImage>
#include <QtConcurrent/QtConcurrentRun>
#include <algorithm>
#include <cmath>

AudioEnvelope::AudioEnvelope(const QString &binId, int clipId, std::pair<int, int> stream, size_t offset, size_t length, size_t startPos)
    : m_offset(offset)
    , m_clipId(clipId)
    , m_startpos(startPos)
{
    std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(binId);
    m_producer = clip->cloneProducer();
    if (length > 2000) {
        // Analyze on timeline clip zone only
        m_offset = 0;
        m_producer->set_in_and_out(int(offset), int(offset + length));
    }
    m_envelopeSize = size_t(m_producer->get_playtime());

    m_producer->set("set.test_image", 1);
    if (stream.first > -1) {
        m_producer->set("audio_index", stream.first);
        m_producer->set("astream", stream.second);
    }
    connect(&m_watcher, &QFutureWatcherBase::finished, this, [this] { Q_EMIT envelopeReady(this); });
    if (!m_producer || !m_producer->is_valid()) {
        qCDebug(KDENLIVE_LOG) << "// Cannot create envelope for producer: " << binId;
    } else {
        m_info = std::make_unique<AudioInfo>(m_producer);
    }
}

AudioEnvelope::~AudioEnvelope()
{
    if (hasComputationStarted()) {
        // This is better than nothing, but does not seem enough to
        // guarantee safe deletion of the AudioEnvelope while the
        // computations are running: if the computations have just
        // finished, m_watcher might be finished, but the signal
        // 'envelopeReady' might still be pending while AudioEnvelope is
        // being deleted, which can cause a crash according to
        // https://doc.qt.io/qt-5/qobject.html#dtor.QObject.
        m_audioSummary.waitForFinished();
        m_watcher.waitForFinished();
    }
}

void AudioEnvelope::startComputeEnvelope()
{
    m_audioSummary = QtConcurrent::run(&AudioEnvelope::loadAndNormalizeEnvelope, this);
    m_watcher.setFuture(m_audioSummary);
}

bool AudioEnvelope::hasComputationStarted() const
{
    // An empty qFuture is canceled. QtConcurrent::run() returns a
    // future that does not support cancellation, so this is a good way
    // to check whether the computations have started.
    return !m_audioSummary.isCanceled();
}

const AudioEnvelope::AudioSummary &AudioEnvelope::audioSummary()
{
    Q_ASSERT(hasComputationStarted());
    m_audioSummary.waitForFinished();
    Q_ASSERT(m_audioSummary.constBegin() != m_audioSummary.constEnd());
    // We use this instead of m_audioSummary.result() in order to return
    // a const reference instead of a copy.
    return *m_audioSummary.constBegin();
}

size_t AudioEnvelope::offset()
{
    return m_offset;
}

const std::vector<qint64> &AudioEnvelope::envelope()
{
    // Blocks until the summary is available.
    return audioSummary().audioAmplitudes;
}

AudioEnvelope::AudioSummary AudioEnvelope::loadAndNormalizeEnvelope() const
{
    qCDebug(KDENLIVE_LOG) << "Loading envelope …";
    AudioSummary summary(m_envelopeSize);
    if (!m_info || m_info->size() < 1) {
        return summary;
    }
    int samplingRate = m_info->info(0)->samplingRate();
    mlt_audio_format format_s16 = mlt_audio_s16;
    int channels = 1;

    QElapsedTimer t;
    t.start();
    m_producer->seek(0);
    size_t max = summary.audioAmplitudes.size();
    for (size_t i = 0; i < max; ++i) {
        std::unique_ptr<Mlt::Frame> frame(m_producer->get_frame(int(i)));
        qint64 position = mlt_frame_get_position(frame->get_frame());
        int samples = mlt_audio_calculate_frame_samples(float(m_producer->get_fps()), samplingRate, position);
        auto *data = static_cast<qint16 *>(frame->get_audio(format_s16, samplingRate, channels, samples));

        summary.audioAmplitudes[i] = 0;
        for (int k = 0; k < samples; ++k) {
            summary.audioAmplitudes[i] += abs(data[k]);
        }
        pCore->displayMessage(i18n("Processing data analysis"), ProcessingJobMessage, int(100 * i / max));
    }
    qCDebug(KDENLIVE_LOG) << "Calculating the envelope (" << m_envelopeSize << " frames) took " << t.elapsed() << " ms.";
    qCDebug(KDENLIVE_LOG) << "Normalizing envelope …";
    const qint64 meanBeforeNormalization =
        std::accumulate(summary.audioAmplitudes.begin(), summary.audioAmplitudes.end(), 0LL) / qint64(summary.audioAmplitudes.size());

    // Normalize the envelope.
    summary.amplitudeMax = 0;
    for (size_t i = 0; i < max; ++i) {
        summary.audioAmplitudes[i] -= meanBeforeNormalization;
        summary.amplitudeMax = std::max(summary.amplitudeMax, qAbs(summary.audioAmplitudes[i]));
    }
    pCore->displayMessage(i18n("Audio analysis finished"), OperationCompletedMessage, 300);
    return summary;
}

int AudioEnvelope::clipId() const
{
    return m_clipId;
}

size_t AudioEnvelope::startPos() const
{
    return m_startpos;
}

QImage AudioEnvelope::drawEnvelope()
{
    const AudioSummary &summary = audioSummary();

    QImage img(int(m_envelopeSize), 400, QImage::Format_ARGB32);
    img.fill(qRgb(255, 255, 255));

    if (summary.amplitudeMax == 0) {
        return img;
    }

    for (int x = 0; x < img.width(); ++x) {
        double fy = double(summary.audioAmplitudes[size_t(x)]) / summary.amplitudeMax * img.height();
        for (int y = img.height() - 1; y > img.height() - 1 - fy; --y) {
            img.setPixel(x, y, qRgb(50, 50, 50));
        }
    }
    return img;
}

void AudioEnvelope::dumpInfo()
{
    if (!m_audioSummary.isFinished()) {
        qCDebug(KDENLIVE_LOG) << "Envelope not yet generated, no information available.";
    } else {
        const AudioSummary &summary = audioSummary();
        qCDebug(KDENLIVE_LOG) << "Envelope info"
                              << "\n* size = " << summary.audioAmplitudes.size() << "\n* max = " << summary.amplitudeMax;
    }
}
