/*
 * Fooyin
 * Copyright © 2023, Luke Taylor <LukeT1@proton.me>
 *
 * Fooyin 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 3 of the License, or
 * (at your option) any later version.
 *
 * Fooyin is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Fooyin.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "ffmpeginput.h"

#include "ffmpegcodec.h"
#include "ffmpegframe.h"
#include "ffmpegstream.h"
#include "ffmpegutils.h"
#include "internalcoresettings.h"

#include <core/constants.h>
#include <core/coresettings.h>
#include <core/engine/audiobuffer.h>

#include <QDebug>
#include <QFile>
#include <QIODevice>

#include <cstring>

#ifdef Q_OS_WINDOWS
#define snprintf _snprintf
#define vsnprintf _vsnprintf
#define strcasecmp _stricmp
#define strncasecmp _strnicmp
#endif

#ifdef __GNUG__
#pragma GCC diagnostic ignored "-Wold-style-cast"
#elifdef __clang__
#pragma clang diagnostic ignored "-Wold-style-cast"
#endif

using namespace Qt::StringLiterals;

constexpr AVRational TimeBaseAv           = {.num = 1, .den = AV_TIME_BASE};
constexpr AVRational TimeBaseMs           = {.num = 1, .den = 1000};
constexpr auto MaxConsecutiveDecodeErrors = 8;

using namespace std::chrono_literals;

namespace {
enum class TagType : uint8_t
{
    Unknown,
    APE,
    ID3v1_1,
    ID3v1_2,
    ID3v2_2,
    ID3v2_3,
    ID3v2_4,
};

bool isRecoverableDecodeError(int error)
{
    return error == AVERROR_INVALIDDATA;
}

QStringList fileExtensions(bool allSupported)
{
    QStringList extensions{u"mp3"_s,  u"ogg"_s,  u"opus"_s, u"oga"_s, u"m4a"_s, u"m4b"_s,  u"wav"_s,
                           u"wv"_s,   u"flac"_s, u"wma"_s,  u"asf"_s, u"mpc"_s, u"aiff"_s, u"ape"_s,
                           u"webm"_s, u"mp4"_s,  u"mka"_s,  u"dsf"_s, u"dff"_s, u"wv"_s};

    if(!allSupported) {
        return extensions;
    }

    const AVInputFormat* format{nullptr};
    void* i{nullptr};

    while((format = av_demuxer_iterate(&i))) {
        if(format->extensions) {
            const QString exts{QString::fromLatin1(format->extensions)};
            const QStringList extList = exts.split(u',', Qt::SkipEmptyParts);
            extensions.append(extList);
        }
    }

    static constexpr std::array extensionBlacklist{
        "ans", "apc",      "aqt",   "aqtitle",  "art",        "asc",      "ass",       "bin",       "bmp_pipe",
        "bmp", "dds_pipe", "diz",   "dpx_pipe", "dss",        "dvbsub",   "exr_pipe",  "exr",       "ffmetadata",
        "gif", "ice",      "ico",   "ilbc",     "image2pipe", "jacosub",  "jpeg_pipe", "jpeg",      "jpg",
        "jxl", "mpl2",     "mpsub", "nfo",      "pjs",        "png_pipe", "png",       "sami",      "smi",
        "srt", "stl",      "sub",   "sub",      "subviewer1", "sup",      "tif",       "tiff_pipe", "tiff",
        "txt", "vt",       "vtt",   "webvtt"};

    for(const auto* ext : extensionBlacklist) {
        extensions.removeAll(QLatin1String{ext});
    }
    extensions.removeDuplicates();

    return extensions;
}

QStringList ffmpegPreferredExtensions()
{
    const Fooyin::FySettings settings;
    return settings
        .value(Fooyin::Settings::Core::Internal::FFmpegPriorityExtensions,
               Fooyin::Settings::Core::Internal::defaultFFmpegPriorityExtensions())
        .toStringList();
}

QString getCodec(AVCodecID codec)
{
    switch(codec) {
        case AV_CODEC_ID_AAC:
            return u"AAC"_s;
        case AV_CODEC_ID_ALAC:
            return u"ALAC"_s;
        case AV_CODEC_ID_MP2:
            return u"MP2"_s;
        case AV_CODEC_ID_MP3:
            return u"MP3"_s;
        case AV_CODEC_ID_WAVPACK:
            return u"WavPack"_s;
        case AV_CODEC_ID_FLAC:
            return u"FLAC"_s;
        case AV_CODEC_ID_OPUS:
            return u"Opus"_s;
        case AV_CODEC_ID_VORBIS:
            return u"Vorbis"_s;
        case AV_CODEC_ID_WMAV2:
            return u"WMA"_s;
        case AV_CODEC_ID_DTS:
            return u"DTS"_s;
        default:
            return {};
    }
}

bool isLossless(AVCodecID codec)
{
    switch(codec) {
        case AV_CODEC_ID_ALAC:
        case AV_CODEC_ID_WAVPACK:
        case AV_CODEC_ID_FLAC:
            return true;
        default:
            return false;
    }
}

bool canBeVBR(const Fooyin::Track& track, AVCodecID codec)
{
    if(track.codecProfile().contains("VBR"_L1) || track.codecProfile().contains("ABR"_L1)) {
        return true;
    }

    switch(codec) {
        case AV_CODEC_ID_AAC: // Assume AAC is always VBR
        case AV_CODEC_ID_ALAC:
        case AV_CODEC_ID_FLAC:
        case AV_CODEC_ID_OPUS:
        case AV_CODEC_ID_VORBIS:
        case AV_CODEC_ID_WAVPACK:
            return true;
        default:
            return false;
    }
}

void readTrackTotalPair(const QString& trackNumbers, Fooyin::Track& track)
{
    const qsizetype splitIdx = trackNumbers.indexOf(u'/');
    if(splitIdx >= 0) {
        track.setTrackNumber(trackNumbers.first(splitIdx));
        track.setTrackTotal(trackNumbers.sliced(splitIdx + 1));
    }
    else if(trackNumbers.size() > 0) {
        track.setTrackNumber(trackNumbers);
    }
}

void readDiscTotalPair(const QString& discNumbers, Fooyin::Track& track)
{
    const qsizetype splitIdx = discNumbers.indexOf(u'/');
    if(splitIdx >= 0) {
        track.setDiscNumber(discNumbers.first(splitIdx));
        track.setDiscTotal(discNumbers.sliced(splitIdx + 1));
    }
    else if(discNumbers.size() > 0) {
        track.setDiscNumber(discNumbers);
    }
}

QString convertString(const char* str)
{
    return QString::fromUtf8(str);
};

float parseReplayGain(const QString& gainStr)
{
    QString string = gainStr.trimmed();
    if(string.endsWith("dB"_L1, Qt::CaseInsensitive)) {
        string.chop(2);
        string = string.trimmed();
    }

    bool ok{false};
    const float gain = string.toFloat(&ok);
    return ok ? gain : Fooyin::Constants::InvalidGain;
}

float parseReplayPeak(const QString& peakStr)
{
    bool ok{false};
    const float peak = peakStr.toFloat(&ok);
    return ok ? peak : Fooyin::Constants::InvalidPeak;
}

enum class TagScope : uint8_t
{
    Global,
    Track
};

QStringList splitMetadata(const QString& string, QChar separator)
{
    QStringList list = string.split(separator, Qt::SkipEmptyParts);
    std::ranges::transform(list, list.begin(), [](QString& entry) { return entry.trimmed(); });
    return list;
}

TagType getTagType(QIODevice* device)
{
    TagType tagType = TagType::Unknown;

    //
    // ID3 1.x
    //

    auto apeOffset = device->size();

    if(device->size() >= 128 && device->seek(device->size() - 128)) {
        auto footer = device->read(8);
        if(footer.startsWith("TAG")) {
            if(device->size() >= 256 && device->seek(device->size() - 256)) {
                auto extFooter = device->read(8);
                // We may have both v1 and v2. Don't return here to let v2 take precedence.
                if(extFooter.startsWith("EXT")) {
                    tagType   = TagType::ID3v1_2;
                    apeOffset = device->size() - 256;
                }
                else {
                    tagType   = TagType::ID3v1_1;
                    apeOffset = device->size() - 128;
                }
            }
        }
    }

    //
    // APE
    //

    if(device->size() >= apeOffset && device->seek(apeOffset - 32)) {
        auto footer = device->read(32);
        if(footer.startsWith("APETAGEX")) {
            return TagType::APE;
        }
    }

    //
    // ID3 2.x
    //

    if(device->seek(0)) {
        auto header = device->read(8);
        if(header.startsWith("ID3")) {
            auto major = static_cast<int>(static_cast<unsigned char>(header.at(3)));
            if(major == 2) {
                return TagType::ID3v2_2;
            }
            if(major == 3) {
                return TagType::ID3v2_3;
            }
            if(major == 4) {
                return TagType::ID3v2_4;
            }
        }
    }

    return tagType;
}

void parseTag(Fooyin::Track& track, TagType tagType, const AVDictionaryEntry* tag, TagScope scope, int chapterCount)
{
    using enum TagType;

    const auto splitArtists = [tagType](const QString& artist) {
        const bool isId3Slash    = tagType == ID3v1_1 || tagType == ID3v1_2 || tagType == ID3v2_2 || tagType == ID3v2_3;
        const bool containsSlash = artist.contains("/"_L1);
        const bool isCommonSlashArtist = artist == "AC/DC"_L1 || artist == "AC / DC"_L1;

        return splitMetadata(artist, isId3Slash && containsSlash && !isCommonSlashArtist ? u'/' : u';');
    };

    if(strcasecmp(tag->key, "album") == 0) {
        track.setAlbum(convertString(tag->value));
    }
    else if(strcasecmp(tag->key, "artist") == 0) {
        const QString artist = convertString(tag->value);
        track.setArtists(splitArtists(artist));
    }
    else if(strcasecmp(tag->key, "album_artist") == 0 || strcasecmp(tag->key, "album artist") == 0) {
        const QString albumArtist = convertString(tag->value);
        track.setAlbumArtists(splitArtists(albumArtist));
    }
    else if(strcasecmp(tag->key, "title") == 0) {
        if(scope == TagScope::Global && chapterCount > 1 && track.album().isEmpty()) {
            track.setAlbum(convertString(tag->value));
        }
        else {
            track.setTitle(convertString(tag->value));
        }
    }
    else if(strcasecmp(tag->key, "genre") == 0) {
        const QString genre = convertString(tag->value);
        track.setGenres(splitMetadata(genre, u';'));
    }
    else if(strcasecmp(tag->key, "part_number") == 0 || strcasecmp(tag->key, "track") == 0) {
        readTrackTotalPair(convertString(tag->value), track);
    }
    else if(strcasecmp(tag->key, "tracktotal") == 0 || strcasecmp(tag->key, "totaltracks") == 0) {
        track.setTrackTotal(convertString(tag->value));
    }
    else if(strcasecmp(tag->key, "disc") == 0 || strcasecmp(tag->key, "discnumber") == 0) {
        readDiscTotalPair(convertString(tag->value), track);
    }
    else if(strcasecmp(tag->key, "disctotal") == 0 || strcasecmp(tag->key, "totaldiscs") == 0) {
        track.setDiscTotal(convertString(tag->value));
    }
    else if(strcasecmp(tag->key, "date") == 0 || strcasecmp(tag->key, "TDRC") == 0
            || strcasecmp(tag->key, "TDRL") == 0) {
        track.setDate(convertString(tag->value));
    }
    else if(strcasecmp(tag->key, "year") == 0) {
        track.setYear(convertString(tag->value).toInt());
    }
    else if(strcasecmp(tag->key, "composer") == 0) {
        const QString composer = convertString(tag->value);
        track.setComposers(splitArtists(composer));
    }
    else if(strcasecmp(tag->key, "performer") == 0) {
        const QString performer = convertString(tag->value);
        track.setPerformers(splitArtists(performer));
    }
    else if(strcasecmp(tag->key, "comment") == 0) {
        track.setComment(convertString(tag->value));
    }
    else if(strcasecmp(tag->key, "encoder") == 0) {
        track.setTool(convertString(tag->value));
    }
    else if(strcasecmp(tag->key, "FMPS_Rating") == 0) {
        track.setRating(convertString(tag->value).toFloat());
    }
    else if(strcasecmp(tag->key, "TFLT") == 0) {
        track.addExtraTag(u"FILETYPE"_s, convertString(tag->value));
    }
    else if(strcasecmp(tag->key, "TOLY") == 0) {
        track.addExtraTag(u"ORIGINALLYRICIST"_s, convertString(tag->value));
    }
    else if(strcasecmp(tag->key, "TLEN") == 0) {
        track.addExtraTag(u"LENGTH"_s, convertString(tag->value));
        track.setDuration(convertString(tag->value).toULongLong());
    }
    else if(strcasecmp(tag->key, "TGID") == 0) {
        track.addExtraTag(u"PODCASTDID"_s, convertString(tag->value));
    }
    else if(strcasecmp(tag->key, "TDES") == 0) {
        track.addExtraTag(u"PODCASTDESC"_s, convertString(tag->value));
    }
    else if(strcasecmp(tag->key, "TCAT") == 0) {
        track.addExtraTag(u"PODCASTCATEGORY"_s, convertString(tag->value));
    }
    else if(strncasecmp(tag->key, "ID3V2_PRIV", 10) == 0) { }
    else if(strcasecmp(tag->key, "REPLAYGAIN_TRACK_GAIN") == 0) {
        track.setRGTrackGain(parseReplayGain(convertString(tag->value)));
    }
    else if(strcasecmp(tag->key, "REPLAYGAIN_TRACK_PEAK") == 0) {
        track.setRGTrackPeak(parseReplayPeak(convertString(tag->value)));
    }
    else if(strcasecmp(tag->key, "REPLAYGAIN_ALBUM_GAIN") == 0) {
        track.setRGAlbumGain(parseReplayGain(convertString(tag->value)));
    }
    else if(strcasecmp(tag->key, "REPLAYGAIN_ALBUM_PEAK") == 0) {
        track.setRGAlbumPeak(parseReplayPeak(convertString(tag->value)));
    }
    else if(strcasecmp(tag->key, "REPLAYGAIN_GAIN") == 0) {
        const float gain = parseReplayGain(convertString(tag->value));
        if(scope == TagScope::Global) {
            track.setRGAlbumGain(gain);
        }
        else {
            track.setRGTrackGain(gain);
        }
    }
    else if(strcasecmp(tag->key, "REPLAYGAIN_PEAK") == 0) {
        const float peak = parseReplayPeak(convertString(tag->value));
        if(scope == TagScope::Global) {
            track.setRGAlbumPeak(peak);
        }
        else {
            track.setRGTrackPeak(peak);
        }
    }
    else {
        track.addExtraTag(convertString(tag->key).toUpper(), convertString(tag->value));
    }
};

bool interleaveSamples(uint8_t* const* in, Fooyin::AudioBuffer& buffer)
{
    const auto format  = buffer.format();
    const int channels = format.channelCount();
    const int samples  = buffer.frameCount();
    const int bps      = format.bytesPerSample();
    auto* out          = buffer.data();

    for(int channel{0}; channel < channels; ++channel) {
        if(!in[channel]) {
            return false;
        }
    }

    const int totalSamples = samples * channels;

    for(int i{0}; i < totalSamples; ++i) {
        const int sampleIndex  = i / channels;
        const int channelIndex = i % channels;

        const auto inOffset  = sampleIndex * bps;
        const auto outOffset = i * bps;
        std::memmove(out + outOffset, in[channelIndex] + inOffset, bps);
    }

    return true;
}

bool interleave(uint8_t* const* in, Fooyin::AudioBuffer& buffer)
{
    if(!in || !buffer.isValid()) {
        return false;
    }

    return interleaveSamples(in, buffer);
}

int ffRead(void* data, uint8_t* buffer, int size)
{
    auto* device = static_cast<QIODevice*>(data);
    QByteArray temp(size, Qt::Uninitialized);
    const auto sizeRead = device->read(temp.data(), size);
    if(sizeRead == 0) {
        return AVERROR_EOF;
    }
    if(sizeRead > 0) {
        std::memcpy(buffer, temp.constData(), static_cast<size_t>(sizeRead));
    }
    return static_cast<int>(sizeRead);
}

int64_t ffSeek(void* data, int64_t offset, int whence)
{
    auto* device = static_cast<QIODevice*>(data);
    int64_t seekPos{0};

    switch(whence) {
        case(AVSEEK_SIZE):
            return device->size();
        case(SEEK_SET):
            seekPos = offset;
            break;
        case(SEEK_CUR):
            seekPos = device->pos() + offset;
            break;
        case(SEEK_END):
            seekPos = device->size() - offset;
            break;
        default:
            return -1;
    }

    if(seekPos < 0 || seekPos > device->size()) {
        return -1;
    }

    return device->seek(seekPos);
}

FormatContext createAVFormatContext(const Fooyin::AudioSource& source)
{
    FormatContext fc;

    fc.ioContext.reset(avio_alloc_context(nullptr, 0, 0, source.device, ffRead, nullptr, ffSeek));
    if(!fc.ioContext) {
        qCWarning(FFMPEG) << "Failed to allocate AVIO context";
        return {};
    }

    AVFormatContext* avContext = avformat_alloc_context();
    if(!avContext) {
        qCWarning(FFMPEG) << "Unable to allocate AVFormat context";
        return {};
    }

    avContext->pb = fc.ioContext.get();

    QByteArray filepathData;
    const char* filepath{nullptr};
    if(!source.filepath.isEmpty()) {
        filepathData = source.filepath.toLocal8Bit();
        filepath     = filepathData.constData();
    }

    const int ret = avformat_open_input(&avContext, filepath, nullptr, nullptr);
    if(ret < 0) {
        // Format context is freed on failure
        char err[AV_ERROR_MAX_STRING_SIZE];
        av_strerror(ret, err, AV_ERROR_MAX_STRING_SIZE);
        qCWarning(FFMPEG) << "Error opening input:" << err;
        return {};
    }

    fc.formatContext.reset(avContext);

    if(avformat_find_stream_info(avContext, nullptr) < 0) {
        qCWarning(FFMPEG) << "Could not find stream info";
        return {};
    }

    // Needed for seeking of APE files
    fc.formatContext->flags |= AVFMT_FLAG_GENPTS;

    // av_dump_format(avContext, 0, source.toUtf8().constData(), 0);

    return fc;
}

QByteArray findCover(AVFormatContext* context, Fooyin::Track::Cover type)
{
    using Cover = Fooyin::Track::Cover;

    const auto count = static_cast<int>(context->nb_streams);

    for(int i{0}; i < count; ++i) {
        AVStream* avStream = context->streams[i];

        if(avStream->disposition & AV_DISPOSITION_ATTACHED_PIC) {
            AVDictionaryEntry* tag{nullptr};
            QString coverType;
            tag = av_dict_get(avStream->metadata, "comment", tag, AV_DICT_IGNORE_SUFFIX);
            if(tag) {
                coverType = convertString(tag->value).toLower();
            }

            if((type == Cover::Front && (coverType.isEmpty() || coverType.contains("front"_L1)))
               || (type == Cover::Back && coverType.contains("back"_L1))
               || (type == Cover::Artist && coverType.contains("artist"_L1))) {
                const AVPacket pkt = avStream->attached_pic;
                if(pkt.size <= 0 || !pkt.data) {
                    return {};
                }
                QByteArray cover(pkt.size, Qt::Uninitialized);
                std::memcpy(cover.data(), pkt.data, static_cast<size_t>(pkt.size));
                return cover;
            }
        }
    }

    return {};
}
} // namespace

namespace Fooyin {
class FFmpegInputPrivate
{
public:
    explicit FFmpegInputPrivate(FFmpegDecoder* self)
        : m_self{self}
    { }

    void reset();
    bool setup(const AudioSource& source);
    void checkIsVbr(const Track& track);

    bool createCodec(AVStream* avStream);

    void decodeAudio(const PacketPtr& packet);
    [[nodiscard]] int sendAVPacket(const PacketPtr& packet) const;
    int receiveAVFrames();

    void readNext();
    void seek(uint64_t pos);

    FFmpegDecoder* m_self;

    IOContextPtr m_ioContext;
    FormatContextPtr m_context;
    Stream m_stream;
    Codec m_codec;
    AudioFormat m_audioFormat;

    AVRational m_timeBase{.num = 0, .den = 0};
    bool m_isSeekable{false};
    bool m_draining{false};
    bool m_error{false};
    bool m_eof{false};
    bool m_isDecoding{false};
    bool m_isVbr{false};
    bool m_returnFrame{false};
    bool m_lastErrorRecoverable{false};

    AudioDecoder::DecoderOptions m_options;
    AudioBuffer m_buffer;
    Frame m_frame;
    int m_bufferPos{0};
    int64_t m_seekPos{-1};
    uint64_t m_currentPos{0};
    int m_bitrate{0};
    int m_skipBytes{0};
    int m_consecutiveDecodeErrors{0};
};

void FFmpegInputPrivate::reset()
{
    m_error                   = false;
    m_eof                     = false;
    m_isDecoding              = false;
    m_draining                = false;
    m_isVbr                   = false;
    m_bitrate                 = 0;
    m_bufferPos               = 0;
    m_currentPos              = 0;
    m_seekPos                 = -1;
    m_skipBytes               = 0;
    m_consecutiveDecodeErrors = 0;
    m_lastErrorRecoverable    = false;
    m_buffer.clear();

    if(m_context) {
        m_context.reset();
    }

    if(m_ioContext) {
        m_ioContext.reset();
    }

    m_stream = {};
    m_codec  = {};
    m_buffer = {};
}

bool FFmpegInputPrivate::setup(const AudioSource& source)
{
    reset();

    FormatContext context = createAVFormatContext(source);
    m_context             = std::move(context.formatContext);
    m_ioContext           = std::move(context.ioContext);

    if(!m_context) {
        return false;
    }

    m_isSeekable = !(m_context->ctx_flags & AVFMTCTX_UNSEEKABLE);

    m_stream = Utils::findAudioStream(m_context.get());
    if(!m_stream.isValid()) {
        return false;
    }

    m_timeBase = m_stream.avStream()->time_base;

    if(createCodec(m_stream.avStream())) {
        m_audioFormat = Utils::audioFormatFromCodec(m_stream.avStream()->codecpar, m_codec.context()->sample_fmt);
        return true;
    }

    return false;
}

void FFmpegInputPrivate::checkIsVbr(const Track& track)
{
    const auto codec = m_codec.context()->codec_id;
    m_isVbr          = canBeVBR(track, codec);
}

bool FFmpegInputPrivate::createCodec(AVStream* avStream)
{
    if(!avStream) {
        return false;
    }

    const AVCodec* avCodec = avcodec_find_decoder(avStream->codecpar->codec_id);
    if(!avCodec) {
        Utils::printError(u"Could not find a decoder for stream"_s);
        m_error = true;
        return false;
    }

    CodecContextPtr avCodecContext{avcodec_alloc_context3(avCodec)};
    if(!avCodecContext) {
        Utils::printError(u"Could not allocate context"_s);
        m_error = true;
        return false;
    }

    if(avCodecContext->codec_type != AVMEDIA_TYPE_AUDIO) {
        return false;
    }

    if(avcodec_parameters_to_context(avCodecContext.get(), avStream->codecpar) < 0) {
        Utils::printError(u"Could not obtain codec parameters"_s);
        m_error = true;
        return {};
    }

    avCodecContext.get()->pkt_timebase = m_timeBase;

    if(avcodec_open2(avCodecContext.get(), avCodec, nullptr) < 0) {
        Utils::printError(u"Could not initialise codec context"_s);
        m_error = true;
        return {};
    }

    m_codec = {std::move(avCodecContext), avStream};

    return true;
}

void FFmpegInputPrivate::decodeAudio(const PacketPtr& packet)
{
    if(!m_isDecoding) {
        return;
    }

    int result = sendAVPacket(packet);

    if(result == AVERROR(EAGAIN)) {
        receiveAVFrames();
        result = sendAVPacket(packet);

        if(result != AVERROR(EAGAIN)) {
            Utils::printError(u"Unexpected decoder behavior"_s);
        }
    }

    if(result == 0) {
        receiveAVFrames();
    }
    else if(result < 0) {
        m_frame = {};
    }
}

int FFmpegInputPrivate::sendAVPacket(const PacketPtr& packet) const
{
    if(m_error || !m_isDecoding) {
        return -1;
    }

    if(!packet || m_draining) {
        return avcodec_send_packet(m_codec.context(), nullptr);
    }

    return avcodec_send_packet(m_codec.context(), packet.get());
}

int FFmpegInputPrivate::receiveAVFrames()
{
    if(m_error || !m_isDecoding) {
        return -1;
    }

    if(!m_codec.isValid()) {
        return -1;
    }

    m_frame          = Frame{m_timeBase};
    const int result = avcodec_receive_frame(m_codec.context(), m_frame.avFrame());

    if(result == AVERROR_EOF) {
        m_draining = true;
        decodeAudio({});
        return result;
    }

    if(result == AVERROR(EAGAIN)) {
        return result;
    }

    if(result < 0) {
        Utils::printError(result);
        m_lastErrorRecoverable = isRecoverableDecodeError(result);

        if(m_lastErrorRecoverable) {
            ++m_consecutiveDecodeErrors;
            if(m_consecutiveDecodeErrors <= MaxConsecutiveDecodeErrors) {
                return result;
            }
        }

        m_error = true;
        return result;
    }

    m_consecutiveDecodeErrors = 0;
    m_lastErrorRecoverable    = false;

    if(!m_returnFrame) {
        const auto sampleCount   = m_audioFormat.bytesPerFrame() * m_frame.sampleCount();
        const uint64_t startTime = m_codec.context()->codec_id == AV_CODEC_ID_APE ? m_currentPos : m_frame.ptsMs();

        if(m_codec.isPlanar()) {
            m_buffer = {m_audioFormat, startTime};
            m_buffer.resize(static_cast<size_t>(sampleCount));

            if(!interleave(m_frame.avFrame()->extended_data, m_buffer)) {
                qCWarning(FFMPEG) << "Invalid planar audio frame";
                m_buffer.clear();
                m_error = true;
                return AVERROR_INVALIDDATA;
            }
        }
        else {
            m_buffer = {m_frame.avFrame()->data[0], static_cast<size_t>(sampleCount), m_audioFormat, startTime};
        }

        if(m_skipBytes > 0) {
            const auto len = std::min(sampleCount, m_skipBytes);
            m_skipBytes -= len;

            if(sampleCount - len == 0) {
                m_buffer.clear();
            }
            else {
                std::memmove(m_buffer.data(), m_buffer.data() + len, sampleCount - len);
                m_buffer.resize(sampleCount - len);
                m_buffer.setStartTime(m_buffer.startTime()
                                      + m_audioFormat.durationForBytes(static_cast<uint64_t>(len)));
            }
        }

        m_currentPos += m_audioFormat.durationForBytes(m_buffer.byteCount());
    }

    return result;
}

void FFmpegInputPrivate::readNext()
{
    if(!m_isDecoding) {
        return;
    }

    // Exhaust the current packet first
    if(receiveAVFrames() == 0) {
        return;
    }

    const PacketPtr packet{av_packet_alloc()};
    const int readResult = av_read_frame(m_context.get(), packet.get());
    if(readResult < 0) {
        if(readResult == AVERROR_EOF && !m_eof) {
            decodeAudio(packet);
            m_eof = true;
        }
        else {
            receiveAVFrames();
            return;
        }
        return;
    }

    m_eof = false;

    if(packet->stream_index != m_codec.streamIndex()) {
        readNext();
        return;
    }

    if(m_isVbr && packet && packet->duration > 0) {
        const auto durSecs = static_cast<double>(av_rescale_q_rnd(packet->duration, m_timeBase, TimeBaseAv,
                                                                  AVRounding::AV_ROUND_NEAR_INF))
                           / static_cast<double>(AV_TIME_BASE);
        if(durSecs > 0) {
            const int bitrate = static_cast<int>((packet->size * 8) / durSecs) / 1000;
            if(bitrate > 0) {
                m_bitrate = bitrate;
            }
        }
    }

    if(m_seekPos > 0 && packet->pts != AV_NOPTS_VALUE) {
        const auto packetPts = av_rescale_q_rnd(packet->pts, m_timeBase, TimeBaseMs, AVRounding::AV_ROUND_DOWN);
        if(packetPts < m_seekPos) {
            m_skipBytes = m_audioFormat.bytesForDuration(static_cast<uint64_t>(m_seekPos - packetPts));
        }
    }
    m_seekPos = -1;

    decodeAudio(packet);
}

void FFmpegInputPrivate::seek(uint64_t pos)
{
    if(!m_context || !m_isSeekable) {
        return;
    }

    // Allow recovery from transient decode errors
    if(m_error && m_lastErrorRecoverable) {
        m_error = false;
    }
    m_consecutiveDecodeErrors = 0;
    m_lastErrorRecoverable    = false;

    m_seekPos = static_cast<int64_t>(pos);

    constexpr static auto min = std::numeric_limits<int64_t>::min();
    constexpr static auto max = std::numeric_limits<int64_t>::max();

    const auto target = av_rescale_q_rnd(m_seekPos, TimeBaseMs, TimeBaseAv, AVRounding::AV_ROUND_DOWN);

    if(auto ret = avformat_seek_file(m_context.get(), -1, min, target, max, 0); ret < 0) {
        Utils::printError(ret);
    }
    avcodec_flush_buffers(m_codec.context());

    m_bufferPos  = 0;
    m_buffer     = {};
    m_eof        = false;
    m_draining   = false;
    m_skipBytes  = 0;
    m_currentPos = pos;
}

FFmpegDecoder::FFmpegDecoder()
    : p{std::make_unique<FFmpegInputPrivate>(this)}
{ }

FFmpegDecoder::~FFmpegDecoder() = default;

QStringList FFmpegDecoder::extensions() const
{
    const FySettings settings;
    return fileExtensions(settings.value(Settings::Core::Internal::FFmpegAllExtensions).toBool());
}

QStringList FFmpegDecoder::preferredExtensions() const
{
    return ffmpegPreferredExtensions();
}

int FFmpegDecoder::bitrate() const
{
    return p->m_bitrate;
}

std::optional<AudioFormat> FFmpegDecoder::init(const AudioSource& source, const Track& track, DecoderOptions options)
{
    p->m_options = options;

    if(p->setup(source)) {
        p->checkIsVbr(track);
        return p->m_audioFormat;
    }

    p->m_error = true;
    return {};
}

void FFmpegDecoder::start()
{
    p->m_isDecoding = true;
}

void FFmpegDecoder::stop()
{
    p->reset();
}

bool FFmpegDecoder::isSeekable() const
{
    return p->m_isSeekable;
}

void FFmpegDecoder::seek(uint64_t pos)
{
    p->seek(pos);
}

Frame FFmpegDecoder::readFrame()
{
    if(!p->m_isDecoding || p->m_error || !p->m_context) {
        return {};
    }

    p->m_returnFrame = true;

    if(!p->m_eof && !p->m_error) {
        p->readNext();
        return p->m_frame;
    }

    return {};
}

AudioBuffer FFmpegDecoder::readBuffer(size_t bytes)
{
    if(!p->m_isDecoding || p->m_error || !p->m_context) {
        return {};
    }

    while(!p->m_buffer.isValid() && !p->m_eof && !p->m_error) {
        p->readNext();
    }

    AudioBuffer buffer;

    const int bytesRequested = static_cast<int>(bytes);
    int bytesWritten{0};

    while(p->m_buffer.isValid() && bytesWritten < bytesRequested) {
        if(!buffer.isValid()) {
            buffer = {p->m_buffer.format(), p->m_buffer.startTime()};
            buffer.reserve(static_cast<size_t>(bytesRequested));
        }
        const int remaining = bytesRequested - bytesWritten;
        const int count     = p->m_buffer.byteCount() - p->m_bufferPos;
        if(count <= remaining) {
            buffer.append(p->m_buffer.data() + p->m_bufferPos, count);
            bytesWritten += count;
            p->m_buffer    = {};
            p->m_bufferPos = 0;
            p->readNext();
        }
        else {
            buffer.append(p->m_buffer.data() + p->m_bufferPos, remaining);
            bytesWritten += remaining;
            p->m_bufferPos += remaining;
        }
    }

    return buffer;
}

std::optional<bool> FFmpegDecoder::isPlanar() const
{
    if(p->m_codec.isValid()) {
        return p->m_codec.isPlanar();
    }

    return {};
}

class FFmpegReaderPrivate
{
public:
    void reset()
    {
        if(m_context) {
            m_context.reset();
        }

        if(m_ioContext) {
            m_ioContext.reset();
        }

        m_stream       = {};
        m_chapterCount = 0;
    }

    bool setup(const AudioSource& source)
    {
        reset();

        FormatContext context = createAVFormatContext(source);
        m_context             = std::move(context.formatContext);
        m_ioContext           = std::move(context.ioContext);

        if(!m_context) {
            return false;
        }

        m_stream = Utils::findAudioStream(m_context.get());
        if(!m_stream.isValid()) {
            return false;
        }

        if(m_context->nb_chapters >= 1) {
            m_chapterCount = static_cast<int>(m_context->nb_chapters);
        }

        return true;
    }

    IOContextPtr m_ioContext;
    FormatContextPtr m_context;
    Stream m_stream;
    int m_chapterCount{0};
};

FFmpegReader::FFmpegReader()
    : p{std::make_unique<FFmpegReaderPrivate>()}
{ }

FFmpegReader::~FFmpegReader() = default;

QStringList FFmpegReader::extensions() const
{
    const FySettings settings;
    return fileExtensions(settings.value(Settings::Core::Internal::FFmpegAllExtensions).toBool());
}

QStringList FFmpegReader::preferredExtensions() const
{
    return ffmpegPreferredExtensions();
}

bool FFmpegReader::canReadCover() const
{
    return true;
}

bool FFmpegReader::canWriteMetaData() const
{
    return false;
}

int FFmpegReader::subsongCount() const
{
    return p->m_chapterCount > 1 ? p->m_chapterCount : 1;
}

bool FFmpegReader::init(const AudioSource& source)
{
    return p->setup(source);
}

bool FFmpegReader::readTrack(const AudioSource& source, Track& track)
{
    if(!p->m_context || !p->m_stream.isValid()) {
        return false;
    }

    const auto* avStream = p->m_stream.avStream();
    const auto* codecPar = p->m_stream.avStream()->codecpar;

    const AVCodec* avCodec = avcodec_find_decoder(avStream->codecpar->codec_id);
    if(!avCodec) {
        Utils::printError(u"Could not find a decoder for stream"_s);
        return false;
    }

    CodecContextPtr avCodecContext{avcodec_alloc_context3(avCodec)};
    if(!avCodecContext) {
        Utils::printError(u"Could not allocate context"_s);
        return false;
    }

    if(avCodecContext->codec_type != AVMEDIA_TYPE_AUDIO) {
        return false;
    }

    if(avcodec_parameters_to_context(avCodecContext.get(), avStream->codecpar) < 0) {
        Utils::printError(u"Could not obtain codec parameters"_s);
        return {};
    }

    if(avcodec_open2(avCodecContext.get(), avCodec, nullptr) < 0) {
        Utils::printError(u"Could not initialise codec context"_s);
        return {};
    }

    const auto format = Utils::audioFormatFromCodec(p->m_stream.avStream()->codecpar, avCodecContext->sample_fmt);

    track.setCodec(getCodec(codecPar->codec_id));
    track.setSampleRate(format.sampleRate());
    track.setChannels(format.channelCount());
    track.setBitDepth(format.bitdepth());
    track.setEncoding(isLossless(codecPar->codec_id) ? u"Lossless"_s : u"Lossy"_s);

    if(track.duration() == 0) {
        AVRational timeBase = avStream->time_base;
        auto duration       = avStream->duration;
        if(duration <= 0 || std::cmp_equal(duration, AV_NOPTS_VALUE)) {
            duration = p->m_context->duration;
            timeBase = TimeBaseAv;
        }
        const auto durationMs = static_cast<uint64_t>(av_rescale_q(duration, timeBase, TimeBaseMs));
        track.setDuration(durationMs);
    }

    auto bitrate = static_cast<int>(codecPar->bit_rate / 1000);
    if(bitrate <= 0) {
        bitrate = static_cast<int>(p->m_context->bit_rate / 1000);
    }
    if(bitrate > 0) {
        track.setBitrate(bitrate);
    }

    const auto tagType = getTagType(source.device);

    const AVDictionaryEntry* tag{nullptr};
    while((tag = av_dict_get(p->m_context->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) {
        parseTag(track, tagType, tag, TagScope::Global, p->m_chapterCount);
    }

    const AVDictionaryEntry* streamTag{nullptr};
    while((streamTag = av_dict_get(avStream->metadata, "", streamTag, AV_DICT_IGNORE_SUFFIX))) {
        parseTag(track, tagType, streamTag, TagScope::Track, p->m_chapterCount);
    }

    const int subsong = track.subsong();
    if(p->m_chapterCount > 1 && subsong >= 0 && subsong < p->m_chapterCount) {
        const AVChapter* chapter = p->m_context->chapters[subsong];

        const auto startMs = static_cast<uint64_t>(av_rescale_q(chapter->start, chapter->time_base, TimeBaseMs));
        const auto endMs   = static_cast<uint64_t>(av_rescale_q(chapter->end, chapter->time_base, TimeBaseMs));

        track.setOffset(startMs);

        // Set default track number before parsing chapter tags
        track.setTrackNumber(QString::number(subsong + 1));

        // Read chapter-level metadata (overrides container tags where defined)
        const AVDictionaryEntry* chapterTag{nullptr};
        while((chapterTag = av_dict_get(chapter->metadata, "", chapterTag, AV_DICT_IGNORE_SUFFIX))) {
            parseTag(track, tagType, chapterTag, TagScope::Track, p->m_chapterCount);
        }

        // Ensure chapter timing and totals are authoritative
        track.setDuration(endMs - startMs);
        track.setTrackTotal(QString::number(p->m_chapterCount));

        track.setIsChapter(true);
    }

    return true;
}

QByteArray FFmpegReader::readCover(const AudioSource& /*source*/, const Track& /*track*/, Track::Cover cover)
{
    if(!p->m_context) {
        return {};
    }

    auto coverData = findCover(p->m_context.get(), cover);

    return coverData;
}

bool FFmpegReader::writeTrack(const AudioSource& /*source*/, const Track& /*track*/, WriteOptions /*options*/)
{
    return false;
}
} // namespace Fooyin
