/*
 * Copyright 2023 Google LLC
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#ifndef skgpu_graphite_PathAtlas_DEFINED
#define skgpu_graphite_PathAtlas_DEFINED

#include "include/core/SkStrokeRec.h"
#include "src/base/SkTInternalLList.h"
#include "src/core/SkTHash.h"
#include "src/gpu/AtlasTypes.h"
#include "src/gpu/ResourceKey.h"
#include "src/gpu/graphite/DrawAtlas.h"
#include "src/gpu/graphite/geom/CoverageMaskShape.h"

namespace skgpu::graphite {

class Caps;
class DrawContext;
class Recorder;
class Rect;
class Renderer;
class Shape;
class TextureProxy;
class Transform;

/**
 * PathAtlas manages one or more atlas textures that store coverage masks for path rendering.
 *
 * The contents of a PathAtlas are intended to be transient: atlas regions are considered valid only
 * for the scope of the render passes that sample them. Unlike DrawAtlas, PathAtlas does not
 * necessarily support partial eviction and reuse of subregions. In most subclasses, once an atlas
 * texture is filled up all of its sub-allocations must be invalidated before it can be reused.
 *
 * PathAtlas does not prescribe how atlas contents get uploaded to the GPU. The specific task
 * mechanism is defined by subclasses.
 */
class PathAtlas {
public:
    /**
     * The PathAtlas will use textures of the requested size or the system's maximum texture size,
     * whichever is smaller.
     */
    PathAtlas(Recorder* recorder, uint32_t requestedWidth, uint32_t requestedHeight);
    virtual ~PathAtlas();

    using MaskAndOrigin = std::pair<CoverageMaskShape, SkIPoint>;

    // Subclasses should ensure that the recorded masks have this much padding around each entry.
    // PathAtlas passes in un-padded sizes to onAddShape and assumes that padding has been included
    // in the outPos value.
    static constexpr int kEntryPadding = 1;

    /**
     * Searches the atlas for a slot that can fit a coverage mask for a clipped shape with the given
     * bounds in device coordinates and submits the mask to be drawn into the found atlas region.
     * For atlases that cache coverage masks, will first search the cache before adding.
     *
     * Returns an empty result if a the shape cannot fit in the atlas. Otherwise, returns the
     * CoverageMaskShape (including the texture proxy) for sampling the eventually-rendered coverage
     * mask and the device-space origin the mask should be drawn at (e.g. its recorded draw should
     * be an integer translation matrix), and the Renderer that should be used to draw that shape.
     * The Renderer should have single-channel coverage, require AA bounds outsetting, and have a
     * single renderStep.
     *
     * The bounds of the atlas entry is laid out with a 1 pixel outset from the given dimensions.
     * The returned shape's UV origin accounts for the padding, and its mask size does not include
     * the padding. This allows the mask to be sampled safely with linear filtering without worrying
     * about HW filtering accessing pixels from other entries.
     *
     * `shape` will be drawn after applying the linear components (scale, rotation, skew) of the
     * provided `localToDevice` transform. This is done by  translating the shape by the inverse of
     * the rounded out `transformedShapeBounds` offset. For an unclipped shape this amounts to
     * translating it back to its origin while preserving any sub-pixel translation. For a clipped
     * shape, this ensures that the visible portions of the mask are centered in the atlas slot
     * while invisible portions that would lie outside the atlas slot get clipped out.
     *
     * `addShape()` schedules the shape to be drawn but when and how the rendering happens is
     * specified by the subclass implementation.
     *
     * The stroke-and-fill style is drawn as a single combined coverage mask containing the stroke
     * and the fill.
     */
    std::pair<const Renderer*, std::optional<MaskAndOrigin>> addShape(
            const Rect& transformedShapeBounds,
            const Shape& shape,
            const Transform& localToDevice,
            const SkStrokeRec& style);

    /**
     * Returns true if a path coverage mask with the given device-space bounds is sufficiently
     * small to benefit from atlasing without causing too many atlas renders.
     *
     * `transformedShapeBounds` represents the device-space bounds of the coverage mask shape
     * unrestricted by clip and viewport bounds.
     *
     * `clipBounds` represents the conservative bounding box of the union of the clip stack that
     * should apply to the shape.
     */
    virtual bool isSuitableForAtlasing(const Rect& transformedShapeBounds,
                                       const Rect& clipBounds) const {
        return true;
    }

    uint32_t width() const { return fWidth; }
    uint32_t height() const { return fHeight; }

protected:
    // The 'transform' has been adjusted to draw the Shape into a logical image from (0,0) to
    // 'maskSize'. The actual rendering into the returned TextureProxy will need to be further
    // translated by the value written to 'outPos', which is the responsibility of subclasses.
    virtual sk_sp<TextureProxy> onAddShape(const Shape&,
                                           const Transform& localToDevice,
                                           const SkStrokeRec&,
                                           skvx::half2 maskOrigin,
                                           skvx::half2 maskSize,
                                           SkIVector transformedMaskOffset,
                                           skvx::half2* outPos) = 0;

    // Wrapper class to manage DrawAtlas and associated caching operations
    class DrawAtlasMgr : public AtlasGenerationCounter, public PlotEvictionCallback {
    public:
        // Adds to the DrawAtlas and shape cache.
        // If successful, returns a ref for the caller to use.
        sk_sp<TextureProxy> findOrCreateEntry(Recorder* recorder,
                                              const Shape&,
                                              const Transform& localToDevice,
                                              const SkStrokeRec&,
                                              skvx::half2 maskOrigin,
                                              skvx::half2 maskSize,
                                              SkIVector transformedMaskOffset,
                                              skvx::half2* outPos);
        // Adds to DrawAtlas but not the cache.
        // If successful, returns a ref for the caller to use.
        sk_sp<TextureProxy> addToAtlas(Recorder* recorder,
                                       const Shape&,
                                       const Transform& localToDevice,
                                       const SkStrokeRec&,
                                       skvx::half2 maskSize,
                                       SkIVector transformedMaskOffset,
                                       skvx::half2* outPos,
                                       AtlasLocator* locator);
        bool recordUploads(DrawContext*, Recorder*);
        void evict(PlotLocator) override;
        void compact(Recorder*);
        void freeGpuResources(Recorder*);

        void evictAll();

    protected:
        DrawAtlasMgr(size_t width, size_t height,
                     size_t plotWidth, size_t plotHeight,
                     DrawAtlas::UseStorageTextures useStorageTextures,
                     std::string_view label, const Caps*);

        bool virtual onAddToAtlas(const Shape&,
                                  const Transform& localToDevice,
                                  const SkStrokeRec&,
                                  SkIRect shapeBounds,
                                  SkIVector transformedMaskOffset,
                                  const AtlasLocator&) = 0;

        std::unique_ptr<DrawAtlas> fDrawAtlas;

    private:
        // Tracks whether a shape is already in the DrawAtlas, and its location in the atlas
        struct UniqueKeyHash {
            uint32_t operator()(const skgpu::UniqueKey& key) const { return key.hash(); }
        };
        using ShapeCache = skia_private::THashMap<skgpu::UniqueKey, AtlasLocator, UniqueKeyHash>;
        ShapeCache fShapeCache;

        // List of stored keys per Plot, used to invalidate cache entries.
        // When a Plot is invalidated via evict(), we'll get its index and Page index from the
        // PlotLocator, index into the fKeyLists array to get the ShapeKeyList for that Plot,
        // then iterate through the list and remove entries matching those keys from the ShapeCache.
        struct ShapeKeyEntry {
            skgpu::UniqueKey fKey;
            SK_DECLARE_INTERNAL_LLIST_INTERFACE(ShapeKeyEntry);
        };
        using ShapeKeyList = SkTInternalLList<ShapeKeyEntry>;
        SkTDArray<ShapeKeyList> fKeyLists;
    };

    // The Recorder that created and owns this Atlas.
    Recorder* fRecorder;

    uint32_t fWidth = 0;
    uint32_t fHeight = 0;
};

}  // namespace skgpu::graphite

#endif  // skgpu_graphite_PathAtlas_DEFINED
