/*
 * Fooyin
 * Copyright © 2024, Luke Taylor <luket@pm.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/>.
 *
 */

#pragma once

#include "fycore_export.h"

#include <core/engine/audiobuffer.h>
#include <core/track.h>

#include <QIODevice>
#include <QObject>

#include <chrono>
#include <memory>
#include <stop_token>
#include <utility>

namespace Fooyin {
class AudioDecoderPrivate;
class ArchiveReader;
class RemoteStreamDevice;

struct ArchiveEntryInfo
{
    QString path;
    uint64_t modifiedTime{0};
    uint64_t size{0};
    bool isRegularFile{false};
};

struct ArchiveEntryData
{
    ArchiveEntryInfo info;
    std::unique_ptr<QIODevice> device;
};

/*!
 * Input source descriptor passed to readers/decoders.
 *
 * `filepath` is always set. `device` is set when caller already opened the
 * source and wants backend code to read from that handle. `archiveReader` is
 * only present for tracks coming from archives.
 */
struct AudioSource
{
    //! Source path (always set).
    QString filepath;
    //! Already-open source device.
    QIODevice* device{nullptr};
    //! Remote stream view for `device`, when available.
    RemoteStreamDevice* remoteStreamDevice{nullptr};
    //! Timeout used while opening/probing remote streams.
    std::chrono::milliseconds remoteOpenTimeout{std::chrono::milliseconds{0}};
    //! Optional archive helper when source originates from an archive container.
    ArchiveReader* archiveReader{nullptr};
    //! Optional source modified time in milliseconds since epoch.
    uint64_t modifiedTime{0};
    //! Optional known source size in bytes.
    uint64_t size{0};
};

/*!
 * Decoder interface used by playback pipeline.
 *
 * Lifecycle:
 * 1. `init()` once per track/subsong
 * 2. optional `start()`
 * 3. repeated `readAudio()`
 * 4. optional `seek()`
 * 5. `stop()` for teardown/reset
 *
 * Runtime playback policy is provided via `PlaybackHints`, which can be
 * injected before and during playback by the engine.
 */
class FYCORE_EXPORT AudioDecoder
{
    Q_GADGET

public:
    enum DecoderFlag : uint8_t
    {
        None = 0,
        //! Disable decoder seeking.
        NoSeeking = 1 << 0,
        //! Disable all loop/repeat behaviour.
        NoLooping = 1 << 1,
        //! Replace infinite looping with backend/default bounded looping.
        NoInfiniteLooping = 1 << 2,
        //! Allow decoder to update `Track` metadata after `init()`.
        //! Useful when duration/fields depend on decoder options.
        UpdateTracks = 1 << 3,
    };
    Q_DECLARE_FLAGS(DecoderOptions, DecoderFlag)
    Q_FLAG(DecoderOptions)

    enum PlaybackHint : uint8_t
    {
        NoHints = 0,
        //! Current playback session is in repeat-track mode.
        RepeatTrackEnabled = 1 << 0,
    };
    Q_DECLARE_FLAGS(PlaybackHints, PlaybackHint)
    Q_FLAG(PlaybackHints)

    enum class ReadStatus : uint8_t
    {
        DecodedAudio = 0,
        NeedMoreInput,
        EndOfStream,
        Error,
    };

    struct ReadResult
    {
        ReadStatus status{ReadStatus::EndOfStream};
        AudioBuffer buffer;
        QString error;

        static ReadResult data(AudioBuffer audioBuffer)
        {
            ReadResult result;
            result.status = ReadStatus::DecodedAudio;
            result.buffer = std::move(audioBuffer);
            return result;
        }

        static ReadResult needMoreInput()
        {
            ReadResult result;
            result.status = ReadStatus::NeedMoreInput;
            return result;
        }

        static ReadResult endOfStream()
        {
            return {};
        }

        static ReadResult errorResult(QString message = {})
        {
            ReadResult result;
            result.status = ReadStatus::Error;
            result.error  = std::move(message);
            return result;
        }
    };

    AudioDecoder();
    virtual ~AudioDecoder();

    /*!
     * Returns a list of file extensions this decoder supports.
     * i.e. "flac,mp3"
     */
    [[nodiscard]] virtual QStringList extensions() const = 0;
    /*!
     * Returns extensions this decoder should be preferred for over higher-priority loaders.
     * Base class implementation returns an empty list.
     */
    [[nodiscard]] virtual QStringList preferredExtensions() const;
    /*!
     * Returns @c true if this decoder can consume remote/network-backed sources
     * supplied by AudioLoader. Base implementation returns @c false.
     */
    [[nodiscard]] virtual bool supportsRemoteSources() const;
    /*!
     * Returns @c true when the decoder needs more input bytes but the source has not
     * reached EOF. The engine should retry later instead of ending playback.
     *
     * Compatibility hook for decoders that only override `readBuffer()`.
     */
    [[nodiscard]] virtual bool needsMoreInput() const;
    /*!
     * Returns @c true if track is seekable.
     * @note Called only after `init()` succeeds.
     */
    [[nodiscard]] virtual bool isSeekable() const = 0;
    /*!
     * Returns @c true if the current track is being repeated/looped forever.
     * @note Called only after `init()` succeeds.
     */
    [[nodiscard]] bool isRepeatingTrack() const;
    /*!
     * Returns @c true if the track passed to `init()` has changed in some way.
     * Useful for properties like duration which may change due to loop count.
     * @note Called only after `init()` succeeds.
     */
    [[nodiscard]] virtual bool trackHasChanged() const;
    /*!
     * Returns the changed track.
     * @note Called only when `trackHasChanged()` returns true.
     */
    [[nodiscard]] virtual Track changedTrack() const;
    /*!
     * Returns the current variable/dynamic bitrate.
     * @note this should return 0 if the file isn't encoded with VBR.
     * @note the base class implementation of this function returns 0.
     */
    [[nodiscard]] virtual int bitrate() const;

    /*!
     * Returns current playback hint flags for this decoder instance.
     */
    [[nodiscard]] PlaybackHints playbackHints() const;
    /*!
     * Updates playback hint flags for this decoder instance.
     */
    void setPlaybackHints(PlaybackHints hints);

    /*!
     * Initialise decoder for `track` and `source`.
     * Called once per track/subsong before decoding starts.
     * @returns a valid AudioFormat if track can be decoded.
     */
    virtual std::optional<AudioFormat> init(const AudioSource& source, const Track& track, DecoderOptions options) = 0;
    /*!
     * Optional start hook after `init()`.
     * Base class implementation does nothing.
     */
    virtual void start();
    /*!
     * Request cancellation of an in-progress blocking decoder operation.
     *
     * This may be called from a control thread while `init()`, `readAudio()`,
     * or `seek()` is running on a worker. It is thread-safe, returns promptly,
     * and does not tear down decoder state.
     */
    void requestAbort();
    /*!
     * Stop and deinitialise decoder state.
     * Should reset to pre-`init()` state.
     * Called on playback stop and track changes.
     */
    virtual void stop() = 0;

    /*!
     * Seek to `pos` milliseconds in current stream.
     */
    virtual void seek(uint64_t pos) = 0;

    /*!
     * Read up to `bytes` of interleaved PCM in the format returned by `init()`.
     *
     * Return `DecodedAudio` for PCM, `NeedMoreInput` when the source is still alive but
     * currently starved, `EndOfStream` only after the decoder has drained all delayed
     * output, or `Error` for fatal decode/source failure.
     *
     * New decoders should override this so EOF, input starvation, and errors are
     * explicit. This is most useful for remote/live streams where temporary input
     * starvation must not be treated as EOF. The default adapts legacy
     * `readBuffer()` decoders.
     */
    virtual ReadResult readAudio(size_t bytes);
    /*!
     * Legacy read API. An invalid buffer means no PCM was returned, but does not
     * identify whether that was EOF, would-block, or an error.
     */
    virtual AudioBuffer readBuffer(size_t bytes) = 0;

protected:
    //! Shared cancellation token for decoder backends and blocking input devices.
    [[nodiscard]] std::stop_token abortToken() const noexcept;

private:
    std::unique_ptr<AudioDecoderPrivate> p;
};
using DecoderCreator = std::function<std::unique_ptr<AudioDecoder>()>;

/*!
 * Metadata/tag reader/writer interface.
 */
class FYCORE_EXPORT AudioReader
{
    Q_GADGET

public:
    enum WriteFlag : uint8_t
    {
        None = 0,
        //! Persist general metadata/tag fields.
        Metadata = 1 << 0,
        //! Persist rating field when supported.
        Rating = 1 << 1,
        //! Persist play count field when supported.
        Playcount = 1 << 2,
        //! Preserve file timestamps (atime/mtime).
        PreserveTimestamps = 1 << 3,
    };
    Q_DECLARE_FLAGS(WriteOptions, WriteFlag)
    Q_FLAG(WriteOptions)

    virtual ~AudioReader() = default;

    /*!
     * Returns a list of file extensions this reader supports.
     * i.e. "flac,mp3"
     */
    [[nodiscard]] virtual QStringList extensions() const = 0;
    /*!
     * Returns extensions this reader should be preferred for over higher-priority loaders.
     * Base class implementation returns an empty list.
     */
    [[nodiscard]] virtual QStringList preferredExtensions() const;
    /*!
     * Returns @c true if this reader can consume remote/network-backed sources
     * supplied by AudioLoader. Base implementation returns @c false.
     */
    [[nodiscard]] virtual bool supportsRemoteSources() const;
    //! True when embedded cover art can be read.
    [[nodiscard]] virtual bool canReadCover() const = 0;
    //! True when embedded cover art can be written.
    [[nodiscard]] virtual bool canWriteCover() const;
    //! True when metadata writing is supported.
    [[nodiscard]] virtual bool canWriteMetaData() const = 0;
    /*!
     * Returns the number of subsongs contained in a file.
     * Called after `init()` and before read/write operations.
     * Base class implementation returns `1` (no subsongs).
     */
    [[nodiscard]] virtual int subsongCount() const;
    /*!
     * Prepares the audio source @p source for reading.
     * If the source has subsongs, expose count via `subsongCount()`.
     * @returns true if source is supported and ready for reads.
     * @note Base class implementation returns true.
     */
    virtual bool init(const AudioSource& source);

    /*!
     * Reads metadata/tags for the given Track @p track.
     * Called only after successful `init()`.
     * @returns whether the track was read successfully.
     */
    [[nodiscard]] virtual bool readTrack(const AudioSource& source, Track& track) = 0;
    /*!
     * Reads embedded artwork for the given Track @p track.
     * Called only after successful `init()`.
     * @returns image data.
     * @note Base class implementation returns empty data.
     */
    [[nodiscard]] virtual QByteArray readCover(const AudioSource& source, const Track& track, Track::Cover cover);
    /*!
     * Writes the metadata/tags in the given Track @p track to file.
     * Called only after successful `init()`.
     * @returns whether the track was written successfully.
     */
    [[nodiscard]] virtual bool writeTrack(const AudioSource& source, const Track& track, WriteOptions options);
    /*!
     * Writes the cover for the given Track @p track to file.
     * Called only after successful `init()`.
     * @returns whether the cover was written successfully.
     */
    [[nodiscard]] virtual bool writeCover(const AudioSource& source, const Track& track, const TrackCovers& covers,
                                          WriteOptions options);
};
using ReaderCreator = std::function<std::unique_ptr<AudioReader>()>;

/*!
 * Reader interface for archive containers that expose files as virtual entries.
 */
class FYCORE_EXPORT ArchiveReader
{
public:
    using ReadEntryCallback      = std::function<void(ArchiveEntryData&&)>;
    using ReadEntryInfoCallback  = std::function<bool(const ArchiveEntryInfo&)>;
    using ShouldContinueCallback = std::function<bool()>;

    virtual ~ArchiveReader() = default;

    /*!
     * Returns a list of file extensions this reader supports.
     * i.e. "zip,7z"
     */
    [[nodiscard]] virtual QStringList extensions() const = 0;
    /*!
     * Returns the current file type.
     * i.e. "zip"
     * @note Called only after `init()` returns true.
     */
    [[nodiscard]] virtual QString type() const = 0;

    /*!
     * Prepares the file @p file for reading.
     * @returns true if file is supported and ready for access.
     */
    virtual bool init(const QString& file) = 0;
    /*!
     * Returns a QIODevice for the file within the archive at @p file.
     * If the file can't be found, this should return nullptr.
     * @note Called only after `init()` returns true.
     */
    virtual ArchiveEntryData entry(const QString& file) = 0;
    /*!
     * Copies the file within the archive at @p file into @p device.
     * If the file can't be found or writing fails, this should return false.
     * @note Called only after `init()` returns true.
     */
    virtual bool copyEntryToDevice(const QString& file, QIODevice* device,
                                   const ShouldContinueCallback& shouldContinue);
    /*!
     * Reads metadata for all entries in the archive.
     * The callback @p readEntry should return false to stop iteration.
     * @returns true if entries were read successfully.
     * @note Called only after `init()` returns true.
     */
    virtual bool readEntries(const ReadEntryInfoCallback& readEntry);
    /*!
     * Reads all files in the archive.
     * The callback @p readEntry should be used to read each file in the archive.
     * @returns true if tracks were read successfully.
     * @note Called only after `init()` returns true.
     */
    virtual bool readTracks(ReadEntryCallback readEntry) = 0;
    /*!
     * Reads artwork within the archive for the given Track @p track.
     * @returns image data.
     * @note Called only after `init()` returns true.
     */
    virtual QByteArray readCover(const Track& track, Track::Cover cover) = 0;
};
using ArchiveReaderCreator = std::function<std::unique_ptr<ArchiveReader>()>;
} // namespace Fooyin

Q_DECLARE_OPERATORS_FOR_FLAGS(Fooyin::AudioDecoder::DecoderOptions)
Q_DECLARE_OPERATORS_FOR_FLAGS(Fooyin::AudioDecoder::PlaybackHints)
Q_DECLARE_OPERATORS_FOR_FLAGS(Fooyin::AudioReader::WriteOptions)
