// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qffmpegmediaplayer_p.h"
#include "private/qplatformaudiooutput_p.h"
#include "qvideosink.h"
#include "qaudiooutput.h"
#include "qaudiobufferoutput.h"
#include "qffmpegplaybackengine_p.h"
#include <qiodevice.h>
#include <qvideosink.h>
#include <QtMultimedia/qplaybackoptions.h>
#include <qtimer.h>
#include <QtConcurrent/QtConcurrent>

#include <qloggingcategory.h>

QT_BEGIN_NAMESPACE

namespace QFFmpeg {

class CancelToken : public ICancelToken
{
public:

    bool isCancelled() const override { return m_cancelled.load(std::memory_order_acquire); }

    void cancel() { m_cancelled.store(true, std::memory_order_release); }

private:
    std::atomic_bool m_cancelled = false;
};

} // namespace QFFmpeg

using namespace QFFmpeg;

QFFmpegMediaPlayer::QFFmpegMediaPlayer(QMediaPlayer *player)
    : QPlatformMediaPlayer(player)
{
    m_positionUpdateTimer.setInterval(50);
    m_positionUpdateTimer.setTimerType(Qt::PreciseTimer);
    connect(&m_positionUpdateTimer, &QTimer::timeout, this, &QFFmpegMediaPlayer::updatePosition);
}

QFFmpegMediaPlayer::~QFFmpegMediaPlayer()
{
    if (m_cancelToken)
        m_cancelToken->cancel();

    m_loadMedia.waitForFinished();
};

qint64 QFFmpegMediaPlayer::duration() const
{
    return m_playbackEngine ? toUserDuration(m_playbackEngine->duration()).get() : 0;
}

void QFFmpegMediaPlayer::setPosition(qint64 position)
{
    if (mediaStatus() == QMediaPlayer::LoadingMedia)
        return;

    if (m_playbackEngine) {
        m_playbackEngine->seek(toTrackPosition(UserTrackPosition(position)));
        updatePosition();
    }

    mediaStatusChanged(QMediaPlayer::LoadedMedia);
}

void QFFmpegMediaPlayer::updatePosition()
{
    positionChanged(m_playbackEngine ? toUserPosition(m_playbackEngine->currentPosition()).get()
                                     : 0);
}

void QFFmpegMediaPlayer::endOfStream()
{
    // stop update timer and report end position anyway
    m_positionUpdateTimer.stop();
    QPointer currentPlaybackEngine(m_playbackEngine.get());
    positionChanged(duration());

    // skip changing state and mediaStatus if playbackEngine has been recreated,
    // e.g. if new media has been loaded as a response to positionChanged signal
    if (currentPlaybackEngine)
        stateChanged(QMediaPlayer::StoppedState);
    if (currentPlaybackEngine)
        mediaStatusChanged(QMediaPlayer::EndOfMedia);
}

void QFFmpegMediaPlayer::onLoopChanged()
{
    // report about finish and start
    // reporting both signals is a bit contraversial
    // but it eshures the idea of notifications about
    // imporatant position points.
    // Also, it ensures more predictable flow for testing.
    positionChanged(duration());
    positionChanged(0);
    m_positionUpdateTimer.stop();
    m_positionUpdateTimer.start();
}

void QFFmpegMediaPlayer::onBuffered()
{
    if (mediaStatus() == QMediaPlayer::BufferingMedia)
        mediaStatusChanged(QMediaPlayer::BufferedMedia);
}

float QFFmpegMediaPlayer::bufferProgress() const
{
    return m_bufferProgress;
}

void QFFmpegMediaPlayer::mediaStatusChanged(QMediaPlayer::MediaStatus status)
{
    if (mediaStatus() == status)
        return;

    const auto newBufferProgress = status == QMediaPlayer::BufferingMedia ? 0.25f // to be improved
            : status == QMediaPlayer::BufferedMedia                       ? 1.f
                                                                          : 0.f;

    if (!QtPrivate::fuzzyCompare(newBufferProgress, m_bufferProgress)) {
        m_bufferProgress = newBufferProgress;
        bufferProgressChanged(newBufferProgress);
    }

    QPlatformMediaPlayer::mediaStatusChanged(status);
}

QMediaTimeRange QFFmpegMediaPlayer::availablePlaybackRanges() const
{
    return {};
}

qreal QFFmpegMediaPlayer::playbackRate() const
{
    return m_playbackRate;
}

void QFFmpegMediaPlayer::setPlaybackRate(qreal rate)
{
    const float effectiveRate = std::max(static_cast<float>(rate), 0.0f);

    if (QtPrivate::fuzzyCompare(m_playbackRate, effectiveRate))
        return;

    m_playbackRate = effectiveRate;

    if (m_playbackEngine)
        m_playbackEngine->setPlaybackRate(effectiveRate);

    playbackRateChanged(effectiveRate);
}

QUrl QFFmpegMediaPlayer::media() const
{
    return m_url;
}

const QIODevice *QFFmpegMediaPlayer::mediaStream() const
{
    return m_device;
}

void QFFmpegMediaPlayer::handleIncorrectMedia(QMediaPlayer::MediaStatus status)
{
    seekableChanged(false);
    audioAvailableChanged(false);
    videoAvailableChanged(false);
    metaDataChanged();
    mediaStatusChanged(status);
    m_playbackEngine = nullptr;
};

void QFFmpegMediaPlayer::setMedia(const QUrl &media, QIODevice *stream)
{
    // Wait for previous unfinished load attempts.
    if (m_cancelToken)
        m_cancelToken->cancel();

    m_loadMedia.waitForFinished();

    m_url = media;
    m_device = stream;
    m_playbackEngine = nullptr;

    if (media.isEmpty() && !stream) {
        handleIncorrectMedia(QMediaPlayer::NoMedia);
        return;
    }

    mediaStatusChanged(QMediaPlayer::LoadingMedia);

    m_requestedStatus = QMediaPlayer::StoppedState;

    m_cancelToken = std::make_shared<CancelToken>();

    // Load media asynchronously to keep GUI thread responsive while loading media
    m_loadMedia = QtConcurrent::run([this, media, stream, cancelToken = m_cancelToken] {
        // On worker thread
        const MediaDataHolder::Maybe mediaHolder =
                MediaDataHolder::create(media, stream, playbackOptions(), cancelToken);

        // Transition back to calling thread using invokeMethod because
        // QFuture continuations back on calling thread may deadlock (QTBUG-117918)
        QMetaObject::invokeMethod(this, [this, mediaHolder, cancelToken] {
            setMediaAsync(mediaHolder, cancelToken);
        });
    });
}

void QFFmpegMediaPlayer::setMediaAsync(QFFmpeg::MediaDataHolder::Maybe mediaDataHolder,
                                       const std::shared_ptr<QFFmpeg::CancelToken> &cancelToken)
{
    // If loading was cancelled, we do not emit any signals about failing
    // to load media (or any other events). The rationale is that cancellation
    // either happens during destruction, where the signals are no longer
    // of interest, or it happens as a response to user requesting to load
    // another media file. In the latter case, we don't want to risk popping
    // up error dialogs or similar.
    if (cancelToken->isCancelled()) {
        return;
    }

    Q_ASSERT(mediaStatus() == QMediaPlayer::LoadingMedia);

    if (!mediaDataHolder) {
        const auto [code, description] = mediaDataHolder.error();
        error(code, description);
        handleIncorrectMedia(QMediaPlayer::MediaStatus::InvalidMedia);
        return;
    }

    m_playbackEngine = std::make_unique<PlaybackEngine>(playbackOptions());

    connect(m_playbackEngine.get(), &PlaybackEngine::endOfStream, this,
            &QFFmpegMediaPlayer::endOfStream);
    connect(m_playbackEngine.get(), &PlaybackEngine::errorOccured, this,
            &QFFmpegMediaPlayer::error);
    connect(m_playbackEngine.get(), &PlaybackEngine::loopChanged, this,
            &QFFmpegMediaPlayer::onLoopChanged);
    connect(m_playbackEngine.get(), &PlaybackEngine::buffered, this,
            &QFFmpegMediaPlayer::onBuffered);

    m_playbackEngine->setMedia(std::move(*mediaDataHolder.value()));

    m_playbackEngine->setAudioBufferOutput(m_audioBufferOutput);
    m_playbackEngine->setAudioSink(m_audioOutput);
    m_playbackEngine->setVideoSink(m_videoSink);

    m_playbackEngine->setLoops(loops());
    m_playbackEngine->setPlaybackRate(m_playbackRate);
    m_playbackEngine->setPitchCompensation(m_pitchCompensation);

    durationChanged(duration());
    tracksChanged();
    metaDataChanged();
    seekableChanged(m_playbackEngine->isSeekable());

    audioAvailableChanged(
            !m_playbackEngine->streamInfo(QPlatformMediaPlayer::AudioStream).isEmpty());
    videoAvailableChanged(
            !m_playbackEngine->streamInfo(QPlatformMediaPlayer::VideoStream).isEmpty());

    mediaStatusChanged(QMediaPlayer::LoadedMedia);

    if (m_requestedStatus != QMediaPlayer::StoppedState) {
        if (m_requestedStatus == QMediaPlayer::PlayingState)
            play();
        else if (m_requestedStatus == QMediaPlayer::PausedState)
            pause();
    }
}

void QFFmpegMediaPlayer::play()
{
    if (mediaStatus() == QMediaPlayer::LoadingMedia) {
        m_requestedStatus = QMediaPlayer::PlayingState;
        return;
    }

    if (!m_playbackEngine)
        return;

    if (mediaStatus() == QMediaPlayer::EndOfMedia && state() == QMediaPlayer::StoppedState) {
        m_playbackEngine->seek(TrackPosition(0));
        positionChanged(0);
    }

    runPlayback();
}

void QFFmpegMediaPlayer::runPlayback()
{
    m_playbackEngine->play();
    m_positionUpdateTimer.start();
    stateChanged(QMediaPlayer::PlayingState);

    if (mediaStatus() == QMediaPlayer::LoadedMedia || mediaStatus() == QMediaPlayer::EndOfMedia)
        mediaStatusChanged(QMediaPlayer::BufferingMedia);
}

void QFFmpegMediaPlayer::pause()
{
    if (mediaStatus() == QMediaPlayer::LoadingMedia) {
        m_requestedStatus = QMediaPlayer::PausedState;
        return;
    }

    if (!m_playbackEngine)
        return;

    if (mediaStatus() == QMediaPlayer::EndOfMedia && state() == QMediaPlayer::StoppedState) {
        m_playbackEngine->seek(TrackPosition(0));
        positionChanged(0);
    }
    m_playbackEngine->pause();
    m_positionUpdateTimer.stop();
    stateChanged(QMediaPlayer::PausedState);

    if (mediaStatus() == QMediaPlayer::LoadedMedia || mediaStatus() == QMediaPlayer::EndOfMedia)
        mediaStatusChanged(QMediaPlayer::BufferingMedia);
}

void QFFmpegMediaPlayer::stop()
{
    if (mediaStatus() == QMediaPlayer::LoadingMedia) {
        m_requestedStatus = QMediaPlayer::StoppedState;
        return;
    }

    if (!m_playbackEngine)
        return;

    m_playbackEngine->stop();
    m_positionUpdateTimer.stop();
    m_playbackEngine->seek(TrackPosition(0));
    positionChanged(0);
    stateChanged(QMediaPlayer::StoppedState);
    mediaStatusChanged(QMediaPlayer::LoadedMedia);
}

void QFFmpegMediaPlayer::setAudioOutput(QPlatformAudioOutput *output)
{
    m_audioOutput = output;
    if (m_playbackEngine)
        m_playbackEngine->setAudioSink(output);
}

void QFFmpegMediaPlayer::setAudioBufferOutput(QAudioBufferOutput *output) {
    m_audioBufferOutput = output;
    if (m_playbackEngine)
        m_playbackEngine->setAudioBufferOutput(output);
}

QMediaMetaData QFFmpegMediaPlayer::metaData() const
{
    return m_playbackEngine ? m_playbackEngine->metaData() : QMediaMetaData{};
}

void QFFmpegMediaPlayer::setVideoSink(QVideoSink *sink)
{
    m_videoSink = sink;
    if (m_playbackEngine)
        m_playbackEngine->setVideoSink(sink);
}

QVideoSink *QFFmpegMediaPlayer::videoSink() const
{
    return m_videoSink;
}

int QFFmpegMediaPlayer::trackCount(TrackType type)
{
    return m_playbackEngine ? m_playbackEngine->streamInfo(type).count() : 0;
}

QMediaMetaData QFFmpegMediaPlayer::trackMetaData(TrackType type, int streamNumber)
{
    if (!m_playbackEngine || streamNumber < 0
        || streamNumber >= m_playbackEngine->streamInfo(type).count())
        return {};
    return m_playbackEngine->streamInfo(type).at(streamNumber).metaData;
}

int QFFmpegMediaPlayer::activeTrack(TrackType type)
{
    return m_playbackEngine ? m_playbackEngine->activeTrack(type) : -1;
}

void QFFmpegMediaPlayer::setActiveTrack(TrackType type, int streamNumber)
{
    if (m_playbackEngine)
        m_playbackEngine->setActiveTrack(type, streamNumber);
    else
        qWarning() << "Cannot set active track without open source";
}

void QFFmpegMediaPlayer::setLoops(int loops)
{
    if (m_playbackEngine)
        m_playbackEngine->setLoops(loops);

    QPlatformMediaPlayer::setLoops(loops);
}

void QFFmpegMediaPlayer::setPitchCompensation(bool enabled)
{
    if (enabled == m_pitchCompensation)
        return;

    m_pitchCompensation = enabled;
    if (m_playbackEngine)
        m_playbackEngine->setPitchCompensation(enabled);
    QPlatformMediaPlayer::pitchCompensationChanged(enabled);
}

bool QFFmpegMediaPlayer::pitchCompensation() const
{
    return m_pitchCompensation;
}

QPlatformMediaPlayer::PitchCompensationAvailability
QFFmpegMediaPlayer::pitchCompensationAvailability() const
{
    return PitchCompensationAvailability::Available;
}

QT_END_NAMESPACE

#include "moc_qffmpegmediaplayer_p.cpp"
