#include "sources/soundsourceopus.h"

#include <QFileInfo>

#include "audio/streaminfo.h"
#include "util/logger.h"

namespace mixxx {

namespace {

const Logger kLogger("SoundSourceOpus");

// Decoded output of opusfile has a fixed sample rate of 48 kHz (fullband)
constexpr audio::SampleRate kSampleRate = audio::SampleRate(48000);

// http://opus-codec.org
//  - Sample rate 48 kHz (fullband)
//  - Frame sizes from 2.5 ms to 60 ms
//   => Up to 48000 kHz * 0.06 s = 2880 sample frames per data frame
// Prefetching 2 * 2880 sample frames while seeking limits the decoding
// errors to kMaxDecodingError during our tests.
//
// According to the API documentation of op_pcm_seek():
// "...decoding after seeking may not return exactly the same
// values as would be obtained by decoding the stream straight
// through. However, such differences are expected to be smaller
// than the loss introduced by Opus's lossy compression."
// This implementation internally uses prefetching to compensate
// those differences, although not completely. The following
// constant indicates the maximum expected difference for
// testing purposes.
constexpr SINT kNumberOfPrefetchFrames = 2 * 2880;

// Parameter for op_channel_count()
// See also: https://mf4.xiph.org/jenkins/view/opus/job/opusfile-unix/ws/doc/html/group__stream__info.html
constexpr int kCurrentStreamLink = -1; // get ... of the current (stream) link

// Parameter for op_pcm_total() and op_bitrate()
// See also: https://mf4.xiph.org/jenkins/view/opus/job/opusfile-unix/ws/doc/html/group__stream__info.html
constexpr int kEntireStreamLink = -1; // get ... of the whole/entire stream

class OggOpusFileOwner {
  public:
    explicit OggOpusFileOwner(OggOpusFile* pFile)
            : m_pFile(pFile) {
    }
    OggOpusFileOwner(OggOpusFileOwner&&) = delete;
    OggOpusFileOwner(const OggOpusFileOwner&) = delete;
    ~OggOpusFileOwner() {
        if (m_pFile) {
            op_free(m_pFile);
        }
    }
    operator OggOpusFile*() const {
        return m_pFile;
    }
    OggOpusFile* release() {
        OggOpusFile* pFile = m_pFile;
        m_pFile = nullptr;
        return pFile;
    }

  private:
    OggOpusFile* m_pFile;
};

} // anonymous namespace

//static
const QString SoundSourceProviderOpus::kDisplayName = QStringLiteral("Xiph.org libopusfile");

//static
const QStringList SoundSourceProviderOpus::kSupportedFileExtensions = {
        QStringLiteral("opus"),
};

SoundSourceProviderPriority SoundSourceProviderOpus::getPriorityHint(
        const QString& supportedFileExtension) const {
    Q_UNUSED(supportedFileExtension)
    // This reference decoder is supposed to produce more accurate
    // and reliable results than any other DEFAULT provider.
    return SoundSourceProviderPriority::Higher;
}

SoundSourceOpus::SoundSourceOpus(const QUrl& url)
        : SoundSource(url),
          m_pOggOpusFile(nullptr),
          m_curFrameIndex(0) {
}

SoundSourceOpus::~SoundSourceOpus() {
    close();
}

std::pair<MetadataSource::ImportResult, QDateTime>
SoundSourceOpus::importTrackMetadataAndCoverImage(
        TrackMetadata* pTrackMetadata,
        QImage* pCoverArt) const {
    auto const imported =
            MetadataSourceTagLib::importTrackMetadataAndCoverImage(
                    pTrackMetadata, pCoverArt);
    if (imported.first == ImportResult::Succeeded) {
        // Done if the default implementation in the base class
        // supports Opus files.
        return imported;
    }

    // Beginning with version 1.9.0 TagLib supports the Opus format.
    // Until this becomes the minimum version required by Mixxx tags
    // in .opus files must also be parsed using opusfile. The following
    // code should removed as soon as it is no longer needed!
    //
    // NOTE(uklotzde): The following code has been found in SoundSourceOpus
    // and will not be improved. We are aware of its shortcomings like
    // the lack of proper error handling.

    // From opus/opusfile.h
    // On Windows, this string must be UTF-8 (to allow access to
    // files whose names cannot be represented in the current
    // MBCS code page).
    // All other systems use the native character encoding.
#ifdef _WIN32
    QByteArray qBAFilename = getLocalFileName().toUtf8();
#else
    QByteArray qBAFilename = QFile::encodeName(getLocalFileName());
#endif

    int errorCode = 0;
    OggOpusFileOwner pOggOpusFile(
            op_open_file(qBAFilename.constData(), &errorCode));
    if (!pOggOpusFile || (errorCode != 0)) {
        kLogger.warning()
                << "Opening of OggOpusFile failed with error"
                << errorCode
                << ":"
                << getLocalFileName();
        // We couldn't do any better , so just return the (unsuccessful)
        // result from the base class.
        return imported;
    }

    // Cast to double is required for duration with sub-second precision
    const double dTotalFrames = op_pcm_total(pOggOpusFile, -1);
    const auto duration = Duration::fromMicros(
            static_cast<qint64>(1000000 * dTotalFrames / kSampleRate));
    pTrackMetadata->setStreamInfo(audio::StreamInfo{
            audio::SignalInfo{
                    audio::ChannelCount(op_channel_count(pOggOpusFile, -1)),
                    kSampleRate,
            },
            audio::Bitrate(op_bitrate(pOggOpusFile, -1) / 1000),
            duration,
    });

#ifndef TAGLIB_HAS_OPUSFILE
    const OpusTags* l_ptrOpusTags = op_tags(pOggOpusFile, -1);
    bool hasDate = false;
    for (int i = 0; i < l_ptrOpusTags->comments; ++i) {
        QString l_SWholeTag = QString(l_ptrOpusTags->user_comments[i]);
        QString l_STag = l_SWholeTag.left(l_SWholeTag.indexOf("="));
        QString l_SPayload = l_SWholeTag.right((l_SWholeTag.length() - l_SWholeTag.indexOf("=")) - 1);

        if (!l_STag.compare("ARTIST")) {
            pTrackMetadata->refTrackInfo().setArtist(l_SPayload);
        } else if (!l_STag.compare("ALBUM")) {
            pTrackMetadata->refAlbumInfo().setTitle(l_SPayload);
        } else if (!l_STag.compare("BPM")) {
            pTrackMetadata->refTrackInfo().setBpm(Bpm(l_SPayload.toDouble()));
        } else if (!l_STag.compare("DATE")) {
            // Prefer "DATE" over "YEAR"
            pTrackMetadata->refTrackInfo().setYear(l_SPayload.trimmed());
            // Avoid to overwrite "DATE" with "YEAR"
            hasDate |= !pTrackMetadata->getTrackInfo().getYear().isEmpty();
        } else if (!hasDate && !l_STag.compare("YEAR")) {
            pTrackMetadata->refTrackInfo().setYear(l_SPayload.trimmed());
        } else if (!l_STag.compare("GENRE")) {
            pTrackMetadata->refTrackInfo().setGenre(l_SPayload);
        } else if (!l_STag.compare("TRACKNUMBER")) {
            pTrackMetadata->refTrackInfo().setTrackNumber(l_SPayload);
        } else if (!l_STag.compare("COMPOSER")) {
            pTrackMetadata->refTrackInfo().setComposer(l_SPayload);
        } else if (!l_STag.compare("ALBUMARTIST")) {
            pTrackMetadata->refAlbumInfo().setArtist(l_SPayload);
        } else if (!l_STag.compare("TITLE")) {
            pTrackMetadata->refTrackInfo().setTitle(l_SPayload);
        } else if (!l_STag.compare("REPLAYGAIN_TRACK_GAIN")) {
            bool gainRatioValid = false;
            double gainRatio = ReplayGain::ratioFromString(l_SPayload, &gainRatioValid);
            if (gainRatioValid) {
                ReplayGain trackGain(pTrackMetadata->getTrackInfo().getReplayGain());
                trackGain.setRatio(gainRatio);
                pTrackMetadata->refTrackInfo().setReplayGain(trackGain);
            }
        } else if (!l_STag.compare("REPLAYGAIN_ALBUM_GAIN")) {
            bool gainRatioValid = false;
            double gainRatio = ReplayGain::ratioFromString(l_SPayload, &gainRatioValid);
            if (gainRatioValid) {
                ReplayGain albumGain(pTrackMetadata->getAlbumInfo().getReplayGain());
                albumGain.setRatio(gainRatio);
                pTrackMetadata->refAlbumInfo().setReplayGain(albumGain);
            }
        }
    }
#endif // TAGLIB_HAS_OPUSFILE

    return std::make_pair(
            ImportResult::Succeeded,
            QFileInfo(getLocalFileName()).lastModified());
}

SoundSource::OpenResult SoundSourceOpus::tryOpen(
        OpenMode /*mode*/,
        const OpenParams& params) {
    // From opus/opusfile.h
    // On Windows, this string must be UTF-8 (to allow access to
    // files whose names cannot be represented in the current
    // MBCS code page).
    // All other systems use the native character encoding.
#ifdef _WIN32
    QByteArray qBAFilename = getLocalFileName().toUtf8();
#else
    QByteArray qBAFilename = QFile::encodeName(getLocalFileName());
#endif

    int errorCode = 0;
    OggOpusFileOwner pOggOpusFile(
            op_open_file(qBAFilename.constData(), &errorCode));
    if (!pOggOpusFile || (errorCode != 0)) {
        kLogger.warning()
                << "Opening of OggOpusFile failed with error"
                << errorCode
                << ":"
                << getLocalFileName();
        return OpenResult::Failed;
    }
    if (!op_seekable(pOggOpusFile)) {
        kLogger.warning()
                << "Stream in"
                << getUrlString()
                << "is not seekable";
        return OpenResult::Aborted;
    }
    DEBUG_ASSERT(!m_pOggOpusFile);
    m_pOggOpusFile = pOggOpusFile.release();

    const int streamChannelCount = op_channel_count(m_pOggOpusFile, kCurrentStreamLink);
    if (0 < streamChannelCount) {
        // opusfile supports to enforce stereo decoding
        bool enforceStereoDecoding =
                params.getSignalInfo().getChannelCount().isValid() &&
                (params.getSignalInfo().getChannelCount() <= 2) &&
                // preserve mono signals if stereo signal is not requested explicitly
                ((params.getSignalInfo().getChannelCount() == 2) || (streamChannelCount > 2));
        if (enforceStereoDecoding) {
            initChannelCountOnce(2);
        } else {
            initChannelCountOnce(streamChannelCount);
        }
    } else {
        kLogger.warning()
                << "Failed to read channel configuration of OggOpus file:"
                << getUrlString();
        return OpenResult::Failed;
    }

    // Reserve enough capacity for buffering a stereo signal!
    const auto prefetchChannelCount = std::min(getSignalInfo().getChannelCount(), audio::ChannelCount(2));
    SampleBuffer(prefetchChannelCount * kNumberOfPrefetchFrames).swap(m_prefetchSampleBuffer);

    const ogg_int64_t pcmTotal = op_pcm_total(m_pOggOpusFile, kEntireStreamLink);
    if (0 <= pcmTotal) {
        initFrameIndexRangeOnce(IndexRange::forward(0, pcmTotal));
    } else {
        kLogger.warning()
                << "Failed to read total length of OggOpus file:"
                << getUrlString();
        return OpenResult::Failed;
    }

    const opus_int32 bitrate = op_bitrate(m_pOggOpusFile, kEntireStreamLink);
    if (0 < bitrate) {
        initBitrateOnce(bitrate / 1000);
    } else {
        kLogger.warning()
                << "Failed to determine bitrate of OggOpus file:"
                << getUrlString();
        return OpenResult::Failed;
    }

    initSampleRateOnce(kSampleRate);

    m_curFrameIndex = frameIndexMin();

    return OpenResult::Succeeded;
}

void SoundSourceOpus::close() {
    if (m_pOggOpusFile) {
        op_free(m_pOggOpusFile);
        m_pOggOpusFile = nullptr;
    }
}

ReadableSampleFrames SoundSourceOpus::readSampleFramesClamped(
        const WritableSampleFrames& writableSampleFrames) {
    const SINT firstFrameIndex = writableSampleFrames.frameIndexRange().start();

    if (m_curFrameIndex != firstFrameIndex) {
        // Prefer skipping over seeking if the seek position is up to
        // 2 * kNumberOfPrefetchFrames in front of the current position
        if ((m_curFrameIndex > firstFrameIndex) ||
                ((firstFrameIndex - m_curFrameIndex) > 2 * kNumberOfPrefetchFrames)) {
            SINT seekIndex = std::max(firstFrameIndex - kNumberOfPrefetchFrames, frameIndexMin());
            int seekResult = op_pcm_seek(m_pOggOpusFile, seekIndex);
            if (0 == seekResult) {
                m_curFrameIndex = seekIndex;
            } else {
                kLogger.warning() << "Failed to seek OggOpus file:" << seekResult;
                const ogg_int64_t pcmOffset = op_pcm_tell(m_pOggOpusFile);
                if (0 <= pcmOffset) {
                    m_curFrameIndex = pcmOffset;
                } else {
                    // Reset to EOF
                    m_curFrameIndex = frameIndexMax();
                }
                // Abort
                return ReadableSampleFrames(
                        IndexRange::between(
                                m_curFrameIndex,
                                m_curFrameIndex));
            }
        }
        // Decoding starts before the actual target position
        // -> skip decoded samples until reaching the target position
        DEBUG_ASSERT(m_curFrameIndex <= firstFrameIndex);
        const auto precedingFrames =
                IndexRange::between(m_curFrameIndex, firstFrameIndex);
        if (!precedingFrames.empty() && (precedingFrames != readSampleFramesClamped(WritableSampleFrames(precedingFrames)).frameIndexRange())) {
            kLogger.warning()
                    << "Failed to skip preceding frames"
                    << precedingFrames;
            return ReadableSampleFrames(IndexRange::between(m_curFrameIndex, m_curFrameIndex));
        }
    }
    DEBUG_ASSERT(m_curFrameIndex == firstFrameIndex);

    const SINT numberOfFramesTotal = writableSampleFrames.frameLength();

    // pSampleBuffer might be null while skipping (see above)
    CSAMPLE* pSampleBuffer = writableSampleFrames.writableData();
    SINT numberOfFramesRemaining = numberOfFramesTotal;
    while (0 < numberOfFramesRemaining) {
        SINT numberOfSamplesToRead =
                getSignalInfo().frames2samples(numberOfFramesRemaining);
        if (!writableSampleFrames.writableData()) {
            // NOTE(uklotzde): The opusfile API does not provide any
            // functions for skipping samples in the audio stream. Calling
            // API functions with a nullptr buffer does not return. Since
            // seeking in Opus files requires prefetching + skipping we
            // need to skip sample frames by reading into a temporary
            // buffer
            pSampleBuffer = m_prefetchSampleBuffer.data();
            if (numberOfSamplesToRead > m_prefetchSampleBuffer.size()) {
                numberOfSamplesToRead = m_prefetchSampleBuffer.size();
            }
        }
        int readResult;
        if (getSignalInfo().getChannelCount() == 2) {
            readResult = op_read_float_stereo(
                    m_pOggOpusFile,
                    pSampleBuffer,
                    numberOfSamplesToRead);
        } else {
            readResult = op_read_float(
                    m_pOggOpusFile,
                    pSampleBuffer,
                    numberOfSamplesToRead,
                    nullptr);
        }
        if (0 < readResult) {
            m_curFrameIndex += readResult;
            pSampleBuffer += getSignalInfo().frames2samples(readResult);
            numberOfFramesRemaining -= readResult;
        } else {
            kLogger.warning() << "Failed to read sample data from OggOpus file:"
                              << readResult;
            break; // abort
        }
    }

    DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex));
    DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining);
    const SINT numberOfFrames = numberOfFramesTotal - numberOfFramesRemaining;
    return ReadableSampleFrames(
            IndexRange::forward(firstFrameIndex, numberOfFrames),
            SampleBuffer::ReadableSlice(
                    writableSampleFrames.writableData(),
                    std::min(writableSampleFrames.writableLength(), getSignalInfo().frames2samples(numberOfFrames))));
}

} // namespace mixxx
