/*****************************************************************************
 * Copyright (C) 2025 VLC authors and VideoLAN
 *
 * 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 2 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/
#ifndef TEXTUREPROVIDEROBSERVER_HPP
#define TEXTUREPROVIDEROBSERVER_HPP

#include <QObject>
#include <QReadWriteLock>
#include <QPointer>
#include <QSize>
#include <QQuickItem>

class QSGTextureProvider;

// This utility class observes a texture provider and exposes its texture's properties
// so that they can be accessed in QML where texture introspection is not possible.
class TextureProviderObserver : public QObject
{
    Q_OBJECT

    // NOTE: source must be a texture provider item.
    // NOTE: If source's texture provider changes, it is required to re-set the property.
    Q_PROPERTY(const QQuickItem* source MEMBER m_source WRITE setSource NOTIFY sourceChanged FINAL)

    // WARNING: Texture properties are updated in the rendering thread.
    // WARNING: Individual properties do not necessarily reflect the same texture at any arbitrary time.
    //          In other words, properties are not updated atomically as a whole but independently
    //          once the source texture changes. It depends on when you are reading the properties,
    //          such that if you read properties before `updateTextureSize()` returns, the properties
    //          may be inconsistent with each other (some reflecting the old texture, some the new
    //          texture). This is done to reduce blocking the GUI thread from the rendering thread,
    //          which is a problem unlike vice versa, and is considered acceptable because sampling
    //          is supposed to be done periodically anyway (the sampling point must be chosen carefully
    //          to not conflict with the updates, if the properties must reflect the immediately up-to-date
    //          texture and the properties change each frame, as otherwise it might end up in a
    //          "forever chase"), so by the time the sampling is done the properties should be consistent.
    // NOTE: If the source texture is a dynamic texture (such as an item layer), these properties are
    //       notified at most per frame, provided that `notifyAllChanges` is set. This is done to prevent
    //       event backlogging. If the source texture is not a dynamic texture, these are notified as soon
    //       as applicable, where in that case `notifyAllChanges` is not required to be set. The method
    //       `::sampleAndNotifyProperties()` may also be called to notify changes at an arbitrary time.
    //       It is guaranteed that notifications are emitted from the GUI thread, unless an arbitrary
    //       `sampleAndNotifyProperties()` call was made in a different thread. Note that if the texture
    //       is a dynamic texture and `notifyAllChanges` is set, the notifications will be signalled from
    //       the thread of the window of the source texture provider item, particularly during
    //       `QQuickWindow::afterAnimating()`, which is guaranteed to be GUI thread but not necessarily
    //       the thread of the items that use this observer. For that reason it is not recommended to
    //       use the observer for an item that belongs to the same window of the source texture provider
    //       item in such a case (texture is dynamic and `notifyAllChanges` is set). Also note that if
    //       the source texture is of `QSGDynamicTexture` type but is not meant to be dynamic (such as
    //       an item layer with `live: false`), it should ideally be provided as a hint to the observer
    //       observer by unsetting `notifyAllChanges`, so that it does not do sampling per frame. That
    //       being said, since event backlogging is still not possible, such benefit is minimal and
    //       should only be done if it does not hinder maintenance.
    Q_PROPERTY(bool notifyAllChanges MEMBER m_notifyAllChanges NOTIFY notifyAllChangesChanged FINAL)
    Q_PROPERTY(QSize textureSize READ textureSize NOTIFY textureSizeChanged FINAL) // Scene graph texture size
    Q_PROPERTY(QSize nativeTextureSize READ nativeTextureSize NOTIFY nativeTextureSizeChanged FINAL) // Native texture size (e.g. for atlas textures, the atlas size)
    Q_PROPERTY(QRectF normalizedTextureSubRect READ normalizedTextureSubRect NOTIFY normalizedTextureSubRectChanged FINAL)
    Q_PROPERTY(qint64 comparisonKey READ comparisonKey NOTIFY comparisonKeyChanged FINAL)

    // NOTE: Since it is not expected that these properties change rapidly, they have notify signals.
    //       These signals may be emitted in the rendering thread, thus if the connection is auto
    //       connection (default), it may be queued in the receiver thread:
    Q_PROPERTY(bool hasAlphaChannel READ hasAlphaChannel NOTIFY hasAlphaChannelChanged FINAL)
    Q_PROPERTY(bool hasMipmaps READ hasMipmaps NOTIFY hasMipmapsChanged FINAL)
    Q_PROPERTY(bool isAtlasTexture READ isAtlasTexture NOTIFY isAtlasTextureChanged FINAL)
    Q_PROPERTY(bool isValid READ isValid NOTIFY isValidChanged FINAL) // whether a texture is provided or not
    Q_PROPERTY(bool isDynamic READ isDynamic NOTIFY isDynamicChanged FINAL) // whether the texture is `QSGDynamicTexture`

public:
    explicit TextureProviderObserver(QObject *parent = nullptr);

    void setSource(const QQuickItem* source, bool enforce = false);

    // The following are likely called in the QML/GUI thread.
    // QML/GUI thread can freely block the rendering thread to the extent the time is reasonable and a
    // fraction of `1/FPS`, because it is already throttled by v-sync (so it would just throttle less).
    QSize textureSize() const;
    QSize nativeTextureSize() const;
    qint64 comparisonKey() const;
    QRectF normalizedTextureSubRect() const;
    bool hasAlphaChannel() const;
    bool hasMipmaps() const;
    bool isAtlasTexture() const;
    bool isValid() const;
    bool isDynamic() const;

public slots:
    // This is mostly relevant for dynamic textures, since for static textures
    // notification is done regardless of sampling. This method is thread-safe.
    void sampleAndNotifyProperties();

signals:
    // Similar to property notify signals, if the texture is dynamic, this
    // signal is compressed and emitted at once once per frame from the GUI
    // thread. If the texture is not dynamic, it is not compressed and emitted
    // from the sg/rendering thread. You may use auto connection to connect
    // to this signal, because there is no risk for backlogging (static
    // textures are not expected to change rapidly).
    void textureChanged();

    void notifyAllChangesChanged();
    void sourceChanged();
    void textureSizeChanged(const QSize&);
    void nativeTextureSizeChanged(const QSize&);
    void normalizedTextureSubRectChanged(const QRectF&);
    void hasAlphaChannelChanged(bool);
    void hasMipmapsChanged(bool);
    void isAtlasTextureChanged(bool);
    void isValidChanged(bool);
    void isDynamicChanged(bool);
    void comparisonKeyChanged(qint64);

private slots:
    void updateProperties();
    void resetProperties(std::memory_order memoryOrder = std::memory_order_seq_cst);
    // This must be called from the same thread the source item lives:
    void adjustSampleAndNotifyConnection();

private:
    QPointer<const QQuickItem> m_source;
    QPointer<QSGTextureProvider> m_provider;
    QPointer<QQuickWindow> m_window;
    QMetaObject::Connection m_windowSampleAndNotifyPropertiesConnection;
    // It is not clear when `QSGTextureProvider::textureChanged()` can be signalled.
    // If it is only signalled during SG synchronization where Qt blocks the GUI thread,
    // we do not need explicit synchronization (with atomic, or mutex) here. If it can be
    // signalled at any time, we can still rely on SG synchronization (instead of explicit
    // synchronization) by waiting until the next synchronization, but the delay might be
    // more in that case. At the same time, the source might be living in a different window
    // where the SG synchronization would not be blocking the (GUI) thread where this
    // observer lives.

    std::atomic<bool> m_textureIsDynamic = false;
    std::atomic<bool> m_notifyAllChanges = true;
    std::atomic<QSize> m_textureSize {{}}; // invalid by default
    std::atomic<QSize> m_oldTextureSize {{}}; // invalid by default
    std::atomic<QSize> m_nativeTextureSize {{}}; // invalid by default
    std::atomic<QSize> m_oldNativeTextureSize {{}}; // invalid by default
    std::atomic<qint64> m_comparisonKey {-1};
    std::atomic<qint64> m_oldComparisonKey {-1};
    std::atomic<QRectF> m_normalizedTextureSubRect {{}}; // invalid by default
    std::atomic<QRectF> m_oldNormalizedTextureSubRect {{}}; // invalid by default

    std::atomic<bool> m_hasAlphaChannel = false;
    std::atomic<bool> m_hasMipmaps = false;
    std::atomic<bool> m_isAtlasTexture = false;
    std::atomic<bool> m_isValid = false;
    std::atomic<bool> m_pendingNotifyTextureChange = false;
};

#endif // TEXTUREPROVIDEROBSERVER_HPP
