/*
 * Copyright (C) 2020 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#pragma once

#if ENABLE(MEDIA_SOURCE)

#include "ExceptionOr.h"
#include "MediaSample.h"
#include "SharedBuffer.h"
#include "SourceBufferParser.h"
#include <CoreAudio/CoreAudioTypes.h>
#include <pal/spi/cf/CoreMediaSPI.h>
#include <variant>
#include <webm/callback.h>
#include <webm/status.h>
#include <webm/vp9_header_parser.h>
#include <wtf/Deque.h>
#include <wtf/MediaTime.h>
#include <wtf/UniqueRef.h>
#include <wtf/Vector.h>

typedef const struct opaqueCMFormatDescription* CMFormatDescriptionRef;

namespace webm {
class WebmParser;
}

namespace WebCore {

class WebMParser : private webm::Callback {
    WTF_MAKE_FAST_ALLOCATED;
public:
    class Callback {
    public:
        virtual void parsedTrimmingData(uint64_t, const MediaTime&) { }
        virtual void parsedInitializationData(SourceBufferParser::InitializationSegment&&) = 0;
        virtual void parsedMediaData(MediaSamplesBlock&&) = 0;
        virtual bool canDecrypt() const { return false; }
        virtual void contentKeyRequestInitializationDataForTrackID(Ref<SharedBuffer>&&, uint64_t) { }
        virtual ~Callback() = default;
    };

    WEBCORE_EXPORT WebMParser(Callback&);
    WEBCORE_EXPORT ~WebMParser();

    class SegmentReader;

    WEBCORE_EXPORT void createByteRangeSamples();
    WEBCORE_EXPORT ExceptionOr<int> parse(SourceBufferParser::Segment&&);
    WEBCORE_EXPORT void resetState();
    WEBCORE_EXPORT void reset();
    WEBCORE_EXPORT void invalidate();
    const webm::Status& status() const { return m_status; }

    void provideMediaData(MediaSamplesBlock&&);

    WEBCORE_EXPORT void setLogger(const Logger&, const void* identifier);
    const Logger* loggerPtr() const { return m_logger.get(); }
    const void* logIdentifier() const { return m_logIdentifier; }

    enum class ErrorCode : int32_t {
        SourceBufferParserWebMErrorCodeStart = 2000,
        InvalidDocType,
        InvalidInitSegment,
        ReceivedEbmlInsideSegment,
        UnsupportedVideoCodec,
        UnsupportedAudioCodec,
        ContentEncrypted,
        VariableFrameDuration,
        ReaderFailed,
        ParserShutdown,
    };

    enum class State : uint8_t {
        None,
        ReadingEbml,
        ReadEbml,
        ReadingSegment,
        ReadingInfo,
        ReadInfo,
        ReadingTracks,
        ReadingTrack,
        ReadTrack,
        ReadingCluster,
    };

    enum class CodecType : uint8_t {
        Unsupported,
        VP8,
        VP9,
        Vorbis,
        Opus,
    };

    class TrackData {
        WTF_MAKE_FAST_ALLOCATED;
    public:
        static auto create(CodecType codecType, const webm::TrackEntry& trackEntry, WebMParser& parser) -> UniqueRef<TrackData>
        {
            return makeUniqueRef<TrackData>(codecType, trackEntry, TrackInfo::TrackType::Unknown, parser);
        }

        TrackData(CodecType codecType, const webm::TrackEntry& trackEntry, TrackInfo::TrackType trackType, WebMParser& parser)
            : m_codec { codecType }
            , m_track { webm::TrackEntry { trackEntry } }
            , m_trackType { trackType }
            , m_parser { parser }
        {
        }
        virtual ~TrackData() = default;

        CodecType codec() const { return m_codec; }
        webm::TrackEntry& track() { return m_track; }
        TrackInfo::TrackType trackType() const { return m_trackType; }

        void createByteRangeSamples() { m_useByteRange = true; }

        RefPtr<TrackInfo> formatDescription() const { return m_formatDescription.copyRef(); }
        void setFormatDescription(Ref<TrackInfo>&& description)
        {
            m_formatDescription = WTFMove(description);
            m_formatDescription->trackID = track().track_uid.value();
        }

        WebMParser& parser() const { return m_parser; }

        virtual webm::Status consumeFrameData(webm::Reader&, const webm::FrameMetadata&, uint64_t*, const MediaTime&)
        {
            ASSERT_NOT_REACHED();
            return webm::Status(webm::Status::kInvalidElementId);
        }

        virtual void resetCompletedFramesState()
        {
            m_completeBlockBuffer = nullptr;
            m_processedMediaSamples = { };
        }

        void reset()
        {
            resetCompletedFramesState();
            m_completePacketSize = std::nullopt;
            m_partialBytesRead = 0;
            m_currentBlockBuffer.reset();
        }

        void drainPendingSamples()
        {
            if (!m_processedMediaSamples.size())
                return;
            m_parser.provideMediaData(WTFMove(m_processedMediaSamples));
            resetCompletedFramesState();
        }

    protected:
        RefPtr<SharedBuffer> contiguousCompleteBlockBuffer(size_t offset, size_t length) const;
        webm::Status readFrameData(webm::Reader&, const webm::FrameMetadata&, uint64_t* bytesRemaining);
        MediaSamplesBlock m_processedMediaSamples;
        bool m_useByteRange { false };
        MediaSamplesBlock::MediaSampleDataType m_completeFrameData;

    private:
        CodecType m_codec;
        webm::TrackEntry m_track;
        const TrackInfo::TrackType m_trackType;
        RefPtr<TrackInfo> m_formatDescription;
        SharedBufferBuilder m_currentBlockBuffer;
        RefPtr<const FragmentedSharedBuffer> m_completeBlockBuffer;
        WebMParser& m_parser;
        std::optional<size_t> m_completePacketSize;
        // Size of the currently incomplete parsed packet.
        size_t m_partialBytesRead { 0 };
    };

    class VideoTrackData : public TrackData {
    public:
        static auto create(CodecType codecType, const webm::TrackEntry& trackEntry, WebMParser& parser) -> UniqueRef<VideoTrackData>
        {
            return makeUniqueRef<VideoTrackData>(codecType, trackEntry, parser);
        }

        VideoTrackData(CodecType codecType, const webm::TrackEntry& trackEntry, WebMParser& parser)
            : TrackData(codecType, trackEntry, TrackInfo::TrackType::Video, parser)
        {
        }

        void flushPendingSamples();

    private:
        const char* logClassName() const { return "VideoTrackData"; }
        webm::Status consumeFrameData(webm::Reader&, const webm::FrameMetadata&, uint64_t*, const MediaTime&) final;
        void resetCompletedFramesState() final;
        void processPendingMediaSamples(const MediaTime&);
        WTF::Deque<MediaSamplesBlock::MediaSampleItem> m_pendingMediaSamples;
        std::optional<MediaTime> m_lastDuration;
        std::optional<MediaTime> m_lastPresentationTime;

#if ENABLE(VP9)
        vp9_parser::Vp9HeaderParser m_headerParser;
#endif
    };

    class AudioTrackData : public TrackData {
    public:
        static auto create(CodecType codecType, const webm::TrackEntry& trackEntry, WebMParser& parser) -> UniqueRef<AudioTrackData>
        {
            return makeUniqueRef<AudioTrackData>(codecType, trackEntry, parser);
        }

        AudioTrackData(CodecType codecType, const webm::TrackEntry& trackEntry, WebMParser& parser)
            : TrackData { codecType, trackEntry, TrackInfo::TrackType::Audio, parser }
        {
        }

    private:
        webm::Status consumeFrameData(webm::Reader&, const webm::FrameMetadata&, uint64_t*, const MediaTime&) final;
        void resetCompletedFramesState() final;
        const char* logClassName() const { return "AudioTrackData"; }

        MediaTime m_packetDuration;
        uint8_t m_framesPerPacket { 0 };
        Seconds m_frameDuration { 0_s };
        size_t mNumFramesInCompleteBlock { 0 };
    };

private:
    TrackData* trackDataForTrackNumber(uint64_t);
    static bool isSupportedVideoCodec(StringView);
    static bool isSupportedAudioCodec(StringView);
    void flushPendingVideoSamples();

    // webm::Callback
    webm::Status OnElementBegin(const webm::ElementMetadata&, webm::Action*) final;
    webm::Status OnElementEnd(const webm::ElementMetadata&) final;
    webm::Status OnEbml(const webm::ElementMetadata&, const webm::Ebml&) final;
    webm::Status OnSegmentBegin(const webm::ElementMetadata&, webm::Action*) final;
    webm::Status OnInfo(const webm::ElementMetadata&, const webm::Info&) final;
    webm::Status OnClusterBegin(const webm::ElementMetadata&, const webm::Cluster&, webm::Action*) final;
    webm::Status OnTrackEntry(const webm::ElementMetadata&, const webm::TrackEntry&) final;
    webm::Status OnBlockBegin(const webm::ElementMetadata&, const webm::Block&, webm::Action*) final;
    webm::Status OnBlockEnd(const webm::ElementMetadata&, const webm::Block&) final;
    webm::Status OnSimpleBlockBegin(const webm::ElementMetadata&, const webm::SimpleBlock&, webm::Action*) final;
    webm::Status OnSimpleBlockEnd(const webm::ElementMetadata&, const webm::SimpleBlock&) final;
    webm::Status OnBlockGroupBegin(const webm::ElementMetadata& , webm::Action*);
    webm::Status OnBlockGroupEnd(const webm::ElementMetadata&, const webm::BlockGroup&);
    webm::Status OnFrame(const webm::FrameMetadata&, webm::Reader*, uint64_t* bytesRemaining) final;

    std::unique_ptr<SourceBufferParser::InitializationSegment> m_initializationSegment;
    Vector<std::pair<uint64_t, Ref<SharedBuffer>>> m_keyIds;
    webm::Status m_status;
    std::unique_ptr<webm::WebmParser> m_parser;
    bool m_initializationSegmentEncountered { false };
    bool m_initializationSegmentProcessed { false };
    uint32_t m_timescale { 1000 };
    uint64_t m_currentTimecode { 0 };

    State m_state { State::None };

    UniqueRef<SegmentReader> m_reader;

    Vector<UniqueRef<TrackData>> m_tracks;
    using BlockVariant = std::variant<webm::Block, webm::SimpleBlock>;
    std::optional<BlockVariant> m_currentBlock;
    std::optional<uint64_t> m_rewindToPosition;

    RefPtr<const Logger> m_logger;
    const void* m_logIdentifier { nullptr };
    uint64_t m_nextChildIdentifier { 0 };
    Callback& m_callback;
    bool m_createByteRangeSamples { false };
};

class SourceBufferParserWebM : public SourceBufferParser, WebMParser::Callback {
    WTF_MAKE_FAST_ALLOCATED;
public:
    static bool isWebMFormatReaderAvailable();
    static MediaPlayerEnums::SupportsType isContentTypeSupported(const ContentType&);
    static Span<const ASCIILiteral> supportedMIMETypes();
    WEBCORE_EXPORT static RefPtr<SourceBufferParserWebM> create(const ContentType&);

    SourceBufferParserWebM();
    ~SourceBufferParserWebM() = default;

    static bool isAvailable();

    Type type() const { return Type::WebM; }
    WEBCORE_EXPORT void appendData(Segment&&, CompletionHandler<void()>&& = [] { }, AppendFlags = AppendFlags::None) final;
    void flushPendingMediaData() final;
    void setShouldProvideMediaDataForTrackID(bool, uint64_t) final;
    bool shouldProvideMediadataForTrackID(uint64_t) final;
    void resetParserState() final { m_parser.resetState(); }
    void invalidate() final;

    using DidParseTrimmingDataCallback = Function<void(uint64_t trackID, const MediaTime& discardPadding)>;
    void setDidParseTrimmingDataCallback(DidParseTrimmingDataCallback&& callback)
    {
        m_didParseTrimmingDataCallback = WTFMove(callback);
    }

    void flushPendingAudioSamples();
    void setMinimumAudioSampleDuration(float);
    
    WEBCORE_EXPORT void setLogger(const Logger&, const void* identifier) final;
    const Logger* loggerPtr() const { return m_logger.get(); }
    const void* logIdentifier() const { return m_logIdentifier; }

private:
    // WebMParser::Callback
    void parsedInitializationData(SourceBufferParser::InitializationSegment&&) final;
    void parsedMediaData(MediaSamplesBlock&&) final;
    bool canDecrypt() const final { return !!m_didProvideContentKeyRequestInitializationDataForTrackIDCallback; }
    void contentKeyRequestInitializationDataForTrackID(Ref<SharedBuffer>&&, uint64_t) final;
    void parsedTrimmingData(uint64_t, const MediaTime&) final;

    void returnSamples(MediaSamplesBlock&&, CMFormatDescriptionRef);

    DidParseTrimmingDataCallback m_didParseTrimmingDataCallback;
    WebMParser m_parser;
    RetainPtr<CMFormatDescriptionRef> m_audioFormatDescription;
    RefPtr<const TrackInfo> m_audioInfo;
    RetainPtr<CMFormatDescriptionRef> m_videoFormatDescription;
    RefPtr<const TrackInfo> m_videoInfo;
    MediaTime m_minimumAudioSampleDuration { 96000, 48000 };
    MediaSamplesBlock m_queuedAudioSamples;
    MediaTime m_queuedAudioDuration;
    RefPtr<const Logger> m_logger;
    const void* m_logIdentifier { nullptr };
};

}

SPECIALIZE_TYPE_TRAITS_BEGIN(WebCore::SourceBufferParserWebM)
    static bool isType(const WebCore::SourceBufferParser& parser) { return parser.type() == WebCore::SourceBufferParser::Type::WebM; }
SPECIALIZE_TYPE_TRAITS_END()

SPECIALIZE_TYPE_TRAITS_BEGIN(WebCore::WebMParser::VideoTrackData)
    static bool isType(const WebCore::WebMParser::TrackData& trackData) { return trackData.trackType() == WebCore::TrackInfo::TrackType::Video; }
SPECIALIZE_TYPE_TRAITS_END()

SPECIALIZE_TYPE_TRAITS_BEGIN(WebCore::WebMParser::AudioTrackData)
    static bool isType(const WebCore::WebMParser::TrackData& trackData) { return trackData.trackType() == WebCore::TrackInfo::TrackType::Audio; }
SPECIALIZE_TYPE_TRAITS_END()

#endif // ENABLE(MEDIA_SOURCE)
