/*
 *  Copyright (C) 2004-2026 Savoir-faire Linux Inc.
 *
 *  This program 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.
 *
 *  This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
 */
#pragma once

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "rational.h"

#ifdef ENABLE_VIDEO
#include "video/video_base.h"
#endif // ENABLE_VIDEO

#ifdef ENABLE_HWACCEL
#include "video/accel.h"
#endif
#include "logger.h"

#include "media_device.h"
#include "media_stream.h"
#include "media_buffer.h"
#include "noncopyable.h"

#include <asio/steady_timer.hpp>

#include <string>
#include <memory>
#include <queue>

extern "C" {
struct AVCodecContext;
struct AVStream;
struct AVDictionary;
struct AVFormatContext;
struct AVCodec;
enum AVMediaType;
}

namespace libjami {
class AudioFrame;
}

namespace jami {

using AudioFrame = libjami::AudioFrame;
#ifdef ENABLE_VIDEO
using VideoFrame = libjami::VideoFrame;
#endif
struct AudioFormat;
class RingBuffer;
class Resampler;
class MediaIOHandle;
class MediaDecoder;

enum class DecodeStatus : uint8_t {
    Success,
    FrameFinished,
    EndOfFile,
    ReadError,
    DecodeError,
    RestartRequired,
    FallBack
};

class MediaDemuxer : public std::enable_shared_from_this<MediaDemuxer>
{
public:
    MediaDemuxer();
    ~MediaDemuxer();

    enum class Status : uint8_t { Success, EndOfFile, ReadBufferOverflow, ReadError, FallBack, RestartRequired };

    static const char* getStatusStr(Status status);

    enum class CurrentState : uint8_t { Demuxing, Finished };
    using StreamCallback = std::function<DecodeStatus(AVPacket&)>;

    int openInput(const DeviceParams&);

    void setInterruptCallback(int (*cb)(void*), void* opaque);
    void setIOContext(MediaIOHandle* ioctx);

    void setKeyFrameRequestCb(std::function<void()> cb);

    void findStreamInfo(bool videoStream = false);
    int selectStream(AVMediaType type);

    void setStreamCallback(unsigned stream, StreamCallback cb = {})
    {
        if (streams_.size() <= stream)
            streams_.resize(stream + 1);
        streams_[stream] = std::move(cb);
    }

    void updateCurrentState(MediaDemuxer::CurrentState state) { currentState_ = state; }

    void setFileFinishedCb(std::function<void(bool)> cb);

    MediaDemuxer::CurrentState getCurrentState() { return currentState_; }

    AVStream* getStream(unsigned stream)
    {
        if (stream >= inputCtx_->nb_streams) {
            JAMI_ERROR("Stream index is out of range: {}", stream);
            return {};
        }
        return inputCtx_->streams[stream];
    }

    Status decode();
    Status demuxe();

    int64_t getDuration() const;
    bool seekFrame(int stream_index, int64_t timestamp);
    void setNeedFrameCb(std::function<void()> cb);
    bool emitFrame(bool isAudio);

private:
    bool streamInfoFound_ {false};
    std::unique_ptr<asio::steady_timer> streamInfoTimer_;
    AVFormatContext* inputCtx_ = nullptr;
    std::vector<StreamCallback> streams_;
    int64_t startTime_;
    int64_t lastReadPacketTime_ {};
    DeviceParams inputParams_;
    AVDictionary* options_ = nullptr;
    MediaDemuxer::CurrentState currentState_;
    std::mutex inputCtxMutex_ {};
    std::mutex audioBufferMutex_ {};
    std::mutex videoBufferMutex_ {};
    std::queue<std::unique_ptr<AVPacket, std::function<void(AVPacket*)>>> videoBuffer_ {};
    std::queue<std::unique_ptr<AVPacket, std::function<void(AVPacket*)>>> audioBuffer_ {};
    std::function<void()> needFrameCb_;
    std::function<void(bool)> fileFinishedCb_;
    std::function<void()> keyFrameRequestCb_;
    void clearFrames();
    bool pushFrameFrom(std::queue<std::unique_ptr<AVPacket, std::function<void(AVPacket*)>>>& buffer,
                       bool isAudio,
                       std::mutex& mutex);
    int baseWidth_ {};
    int baseHeight_ {};
};

class MediaDecoder
{
public:
    MediaDecoder();
    MediaDecoder(MediaObserver observer);
    MediaDecoder(const std::shared_ptr<MediaDemuxer>& demuxer, int index);
    MediaDecoder(const std::shared_ptr<MediaDemuxer>& demuxer, int index, MediaObserver observer);
    MediaDecoder(const std::shared_ptr<MediaDemuxer>& demuxer, AVMediaType type)
        : MediaDecoder(demuxer, demuxer->selectStream(type))
    {}
    ~MediaDecoder();

    void emulateRate() { emulateRate_ = true; }

    int openInput(const DeviceParams&);
    void setInterruptCallback(int (*cb)(void*), void* opaque);
    void setIOContext(MediaIOHandle* ioctx);

    int setup(AVMediaType type);
    int setupAudio() { return setup(AVMEDIA_TYPE_AUDIO); }
    int setupVideo() { return setup(AVMEDIA_TYPE_VIDEO); }
    void setKeyFrameRequestCb(std::function<void()> cb);

    MediaDemuxer::Status decode();
    DecodeStatus flush();

    int getWidth() const;
    int getHeight() const;
    std::string getDecoderName() const;

    rational<double> getFps() const;
    AVPixelFormat getPixelFormat() const;

    void updateStartTime(int64_t startTime);

    bool emitFrame(bool isAudio);
    void flushBuffers();
    void setSeekTime(int64_t time);
#ifdef ENABLE_HWACCEL
    void enableAccel(bool enableAccel);
#endif

    MediaStream getStream(const std::string& name = "") const;

    void setResolutionChangedCallback(std::function<void(int, int)> cb) { resolutionChangedCallback_ = std::move(cb); }

    void setFEC(bool enable) { fecEnabled_ = enable; }

    void setContextCallback(const std::function<void()>& cb)
    {
        firstDecode_.exchange(true);
        contextCallback_ = cb;
    }

private:
    NON_COPYABLE(MediaDecoder);

    DecodeStatus decode(AVPacket&);

    rational<unsigned> getTimeBase() const;

    std::shared_ptr<MediaDemuxer> demuxer_;

    const AVCodec* inputDecoder_ = nullptr;
    AVCodecContext* decoderCtx_ = nullptr;
    AVStream* avStream_ = nullptr;
    bool emulateRate_ = false;
    bool passthrough_ = false;
    int64_t startTime_;
    int64_t lastTimestamp_ {0};

    DeviceParams inputParams_;

    int correctPixFmt(int input_pix_fmt);
    int setupStream();

    bool fallback_ = false;

#ifdef ENABLE_HWACCEL
    bool enableAccel_ = true;
    std::unique_ptr<video::HardwareAccel> accel_;
    unsigned short accelFailures_ = 0;
#endif
    MediaObserver callback_;
    int prepareDecoderContext();
    int64_t seekTime_ = -1;
    void resetSeekTime() { seekTime_ = -1; }
    std::function<void(int, int)> resolutionChangedCallback_;

    int width_;
    int height_;

    bool fecEnabled_ {false};

    std::function<void()> contextCallback_;
    std::atomic_bool firstDecode_ {true};

protected:
    AVDictionary* options_ = nullptr;
};

} // namespace jami
