// MobileGL - MobileGL/MG_State/GLState/TextureState/TextureObject.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
//   https://www.gnu.org/licenses/gpl-3.0.txt
//   https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header

#pragma once
#include "TextureEnum.h"
#include "MipmapUploadTargetArray.h"
#include "MG_Util/Types.h"
#include "../SamplerState/SamplerObject.h"
#include <Includes.h>
#include <MG_Util/Math/VectorTypes.h>

namespace MobileGL::MG_State::GLState {
    // Texture objects are always SharedPtr-owned (TextureState creates every instance via
    // MakeShared, including the per-target default objects). enable_shared_from_this lets
    // backends that only receive a reference (e.g. syncing a name-deleted texture kept
    // alive by an FBO attachment) still register a weak liveness reference for GC.
    class ITextureObject : public std::enable_shared_from_this<ITextureObject> {
    public:
        using TargetEnum = TextureTarget;
        virtual ~ITextureObject() = default;

        virtual TextureStorageType GetStorageType() const = 0;

        virtual TextureInternalFormat GetFormat() const = 0;
        virtual TextureTarget GetTarget() const = 0;
        virtual const Vector<TextureUploadTarget>& GetUploadTargets() const = 0;
        virtual IntVec3 GetBaseSize() const = 0;
        virtual const SharedPtr<SamplerObject>& GetSamplerObject() const = 0;
        virtual void SetInternalFormat(TextureInternalFormat format) = 0;
        virtual Bool IsComplete() const = 0;
        virtual Uint GetExternalIndex() const = 0;
        virtual const FloatVec4& GetBorderColor() const = 0;
        virtual void SetBorderColor(const FloatVec4& color) = 0;
        virtual const IntVec4& GetBorderColorI() const = 0;
        virtual void SetBorderColorI(const IntVec4& color) = 0;
        virtual const UintVec4& GetBorderColorUI() const = 0;
        virtual void SetBorderColorUI(const UintVec4& color) = 0;
        // Which of the three setters above last ran; see SamplerParameters::borderColorForm.
        virtual BorderColorForm GetBorderColorForm() const = 0;
        virtual TextureSwizzleParam GetSwizzleParam(TextureSwizzleParam param) const = 0;
        virtual void SetSwizzleParam(TextureSwizzleParam param, TextureSwizzleParam value) = 0;
        virtual void SetSwizzleParamRGBA(const Vec4<TextureSwizzleParam>& values) = 0;
        virtual const Vec4<TextureSwizzleParam>& GetAllSwizzleParams() const = 0;
        virtual const UintVec2& GetLevelRange() const = 0;
        virtual void SetBaseLevel(Uint baseLevel) = 0;
        virtual void SetMaxLevel(Uint maxLevel) = 0;
        virtual Bool IsImmutable() const = 0;
        virtual Uint GetImmutableLevels() const = 0;
        // How many levels THIS object can address, i.e. the bound a level argument has to
        // stay under. The same number as GetImmutableLevels() for an ordinary immutable
        // texture, but NOT for a view: GL 4.6 core 8.18 defines TEXTURE_IMMUTABLE_LEVELS on a
        // view as the ORIGINAL texture's value, which says nothing about what the view itself
        // can reach, and bounding by it lets a level the view does not have through.
        virtual Uint GetAddressableLevelCount() const = 0;
        virtual void SetImmutableLevels(Uint levels) = 0;
        virtual Uint16 GetTextureParamsVersion() const = 0;
        // Monotonic counter bumped on every CPU-side pixel mutation (see MarkStorageDirty).
        // Backends compare it against a per-resource snapshot to skip re-syncing unchanged
        // textures across draws (e.g. the block atlas bound across a whole terrain batch).
        virtual Uint64 GetContentVersion() const = 0;
        // Monotonic counter bumped on every SHAPE mutation - level sizes, the stored level
        // set, the internal format, the level range (see BumpShapeVersion). Disjoint from the
        // content version on purpose: glTexImage2D(..., nullptr) re-specifies a level's size
        // without dirtying a single texel, so a backend that keys its "nothing changed since
        // the last sync" skip on content alone keeps a resource of the OLD size alive.
        virtual Uint64 GetShapeVersion() const = 0;
        // Answers IsMipmapCompleteForFilter() from a memo. Sampling completeness is a
        // property of the texture's SHAPE - level sizes, level count, level range,
        // internal format - and never of its texel content, but every draw asks about
        // every bound texture, which made recomputing it one of the hottest things both
        // backends did (the walk plus its GetTexelSize calls measured ~8% of the render
        // thread). Shape mutations invalidate the memo; uploads do not.
        virtual Bool IsMipmapCompleteForFilterCached(Bool mipmapped) const = 0;
        virtual Int GetSamples() const = 0;
        virtual void SetSamples(Int samples) = 0;
        virtual Bool HasFixedSampleLocations() const = 0;
        virtual void SetFixedSampleLocations(Bool fixedSampleLocations) = 0;
        virtual Uint64 GetLifetimeId() const = 0;
        // Which aspect of a packed depth/stencil texture a sampler reads (GL 4.6 core 8.10).
        // DEPTH_COMPONENT until set, and meaningless for every other format.
        virtual GLenum GetDepthStencilTextureMode() const = 0;
        virtual void SetDepthStencilTextureMode(GLenum mode) = 0;

        // ---- Texture views (ARB_texture_view / GL 4.6 core 8.18) ----
        // The texture object whose immutable storage this one's texels actually live in, or
        // nullptr when this texture owns its storage. It is itself NEVER a view: glTextureView
        // composes a view-of-a-view onto the ROOT at creation, which is exactly what the spec's
        // additive "<minlevel> plus the value of TEXTURE_VIEW_MIN_LEVEL from the original
        // texture" rule describes, so one hop always reaches the storage.
        //
        // Holding it as a SharedPtr is what gives GL's name-deletion semantics for free: after
        // glDeleteTextures(origtexture) the name is gone and TextureState has dropped its entry,
        // but the object - and therefore the storage and every backend resource keyed on it -
        // stays alive as long as some view still references it (GL 4.6 core 5.1.2).
        virtual const SharedPtr<ITextureObject>& GetViewStorageOwner() const = 0;
        Bool IsTextureView() const { return GetViewStorageOwner() != nullptr; }
        // GL 4.6 core table 23.17, expressed in the storage owner's level/layer coordinates
        // (see above - composition makes the two the same number). All four are 0 on a mutable
        // texture; TexStorage* seeds them with (0, levels, 0, layers) because the spec makes an
        // immutable texture a full-extent view of itself, and glTextureView composes onto those.
        virtual Uint GetViewMinLevel() const = 0;
        virtual Uint GetViewNumLevels() const = 0;
        virtual Uint GetViewMinLayer() const = 0;
        virtual Uint GetViewNumLayers() const = 0;
        virtual void SetViewLevelLayerRange(Uint minLevel, Uint numLevels, Uint minLayer, Uint numLayers) = 0;

    protected:
        virtual Uint GetIndexOfTextureUploadTarget(TextureUploadTarget target) const = 0;
    };

    class TextureObjectBase : public ITextureObject {
    public:
        TextureObjectBase(TextureTarget target, Uint externalIndex);
        virtual ~TextureObjectBase() = default;

        TextureInternalFormat GetFormat() const override;
        TextureTarget GetTarget() const override;
        IntVec3 GetBaseSize() const override;
        const SharedPtr<SamplerObject>& GetSamplerObject() const override;
        void SetInternalFormat(TextureInternalFormat format) override;
        Bool IsComplete() const override;
        Uint GetExternalIndex() const override;
        const FloatVec4& GetBorderColor() const override;
        void SetBorderColor(const FloatVec4& color) override;
        const IntVec4& GetBorderColorI() const override;
        void SetBorderColorI(const IntVec4& color) override;
        const UintVec4& GetBorderColorUI() const override;
        void SetBorderColorUI(const UintVec4& color) override;
        BorderColorForm GetBorderColorForm() const override;
        TextureSwizzleParam GetSwizzleParam(TextureSwizzleParam param) const override;
        const Vec4<TextureSwizzleParam>& GetAllSwizzleParams() const override;
        void SetSwizzleParam(TextureSwizzleParam param, TextureSwizzleParam value) override;
        void SetSwizzleParamRGBA(const Vec4<TextureSwizzleParam>& values) override;
        const UintVec2& GetLevelRange() const override;
        void SetBaseLevel(Uint baseLevel) override;
        void SetMaxLevel(Uint maxLevel) override;
        Bool IsImmutable() const override;
        Uint GetImmutableLevels() const override;
        // m_immutableLevels is already the VIEW-relative count for a view (its constructor
        // stores <numlevels> there so the level-range clamp works in view coordinates), so
        // this one accessor is correct for both and needs no override.
        Uint GetAddressableLevelCount() const override { return m_immutableLevels; }
        void SetImmutableLevels(Uint levels) override;
        Uint16 GetTextureParamsVersion() const override;
        Uint64 GetContentVersion() const override;
        Uint64 GetShapeVersion() const override;
        Bool IsMipmapCompleteForFilterCached(Bool mipmapped) const override;
        // Bumps the content version without touching per-level storage-dirty flags. Used when the
        // set of defined mip levels grows via GPU-side mip generation (glGenerateMipmap): the level
        // set changed (so a cached sampled view's level range is stale) but no CPU data is dirty.
        void BumpContentVersion();
        Int GetSamples() const override;
        void SetSamples(Int samples) override;
        Bool HasFixedSampleLocations() const override;
        void SetFixedSampleLocations(Bool fixedSampleLocations) override;
        Uint64 GetLifetimeId() const override;
        // A plain texture owns its storage; TextureObjectView overrides this.
        const SharedPtr<ITextureObject>& GetViewStorageOwner() const override;
        Uint GetViewMinLevel() const override { return m_viewMinLevel; }
        Uint GetViewNumLevels() const override { return m_viewNumLevels; }
        Uint GetViewMinLayer() const override { return m_viewMinLayer; }
        Uint GetViewNumLayers() const override { return m_viewNumLayers; }
        void SetViewLevelLayerRange(Uint minLevel, Uint numLevels, Uint minLayer, Uint numLayers) override {
            m_viewMinLevel = minLevel;
            m_viewNumLevels = numLevels;
            m_viewMinLayer = minLayer;
            m_viewNumLayers = numLayers;
        }
        GLenum GetDepthStencilTextureMode() const override { return m_depthStencilTextureMode; }
        // Bumps the params version like every other backend-visible texture parameter: the mode
        // decides which ASPECT of a packed depth/stencil image a sampler reads, which DirectGLES
        // forwards as a texture parameter and DirectVulkan bakes into the sampled image view. A
        // silent write here would leave both backends showing the aspect they last built.
        void SetDepthStencilTextureMode(GLenum mode) override {
            if (m_depthStencilTextureMode == mode) return;
            m_depthStencilTextureMode = mode;
            ++m_textureParamsVersion;
        }

    protected:
        static Uint64 AllocateLifetimeId();
        // The ONLY way m_shapeVersion may move. Besides invalidating this object's own
        // completeness memo it bumps the context-wide sampling-resolution generation, which is
        // what a backend memo of the resolved per-unit bindings watches: completeness decides
        // whether a bound texture reaches its native target at all, and a shape change is
        // otherwise invisible to such a memo (no bind moved).
        void BumpShapeVersion();

        const Uint m_externalIndex;
        const Uint64 m_lifetimeId;
        const TextureTarget m_target = TextureTarget::Unknown;
        TextureInternalFormat m_internalFormat = TextureInternalFormat::Unknown;
        SharedPtr<SamplerObject> m_sampler = nullptr;
        Vec4<TextureSwizzleParam> m_swizzleParams = {TextureSwizzleParam::Red, TextureSwizzleParam::Green,
                                                     TextureSwizzleParam::Blue, TextureSwizzleParam::Alpha};
        UintVec2 m_levelRange = {0, 1000};
        Uint m_immutableLevels = 0;
        Uint16 m_textureParamsVersion = 0;
        // Bumped by every mutation the completeness answer depends on - internal
        // format, level range, and the stored level set - and by nothing else, so a
        // texel upload leaves the memo below valid.
        Uint64 m_shapeVersion = 1;
        // [0] = the non-mipmapped answer, [1] = the mipmapped one. Mutable because
        // completeness is a query; a zero version means "never computed".
        mutable Uint64 m_completeMemoShapeVersion[2] = {0, 0};
        mutable Bool m_completeMemoValue[2] = {false, false};
        // Starts at 1 so a freshly-created backend resource (snapshot 0) never spuriously
        // matches before its first sync. Bumped only on dirty=true in MarkStorageDirty.
        Uint64 m_contentVersion = 1;
        GLenum m_depthStencilTextureMode = GL_DEPTH_COMPONENT;
        // GL 4.6 core table 23.17: all four are 0 until immutable storage exists, which is what
        // makes glGetTexParameteriv(GL_TEXTURE_VIEW_NUM_LEVELS) answer 0 on a mutable texture.
        Uint m_viewMinLevel = 0;
        Uint m_viewNumLevels = 0;
        Uint m_viewMinLayer = 0;
        Uint m_viewNumLayers = 0;
        Int m_samples = 0;
        Bool m_fixedSampleLocations = true;
    };

    class TextureObjectMipmap : public TextureObjectBase {
    public:
        TextureObjectMipmap(TextureTarget target, Uint externalIndex) : TextureObjectBase(target, externalIndex) {}

        TextureStorageType GetStorageType() const override { return TextureStorageType::Mipmap; }

        virtual Uint GetMipmapLevelCount() const = 0;
        virtual const IntVec3 GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const = 0;
        virtual const SizeT GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const = 0;
        virtual void AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) = 0;
        // AllocateStorage only ever grows the chain. Callers that define the complete level set -
        // glTexStorage*, mip regeneration, or a level-0 respecification at a new size - drop the
        // leftovers explicitly, so a stale tail can never make the texture silently incomplete.
        virtual void TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) = 0;
        virtual void UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) = 0;
        virtual void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) = 0;
        virtual void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty = true) = 0;
        virtual Bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
        // Sub-image variant of MarkStorageDirty(..., true): backends may then upload
        // only the accumulated region instead of the whole level. The base fallback
        // keeps whole-level semantics for storage classes that do not track regions.
        virtual void MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, IntVec3 offset,
                                            IntVec3 size) {
            (void)offset;
            (void)size;
            MarkStorageDirty(uploadTarget, mipmapLevel, true);
        }
        // Meaningful only while IsStorageDirty(uploadTarget, mipmapLevel).
        virtual MipmapDirtyRegion GetStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel) const {
            const IntVec3 size = GetMipmapTexelSize(uploadTarget, mipmapLevel);
            return {IntVec3{0, 0, 0}, IntVec3{size.x(), size.y(), std::max(size.z(), 1)}};
        }
        // Scatter detail behind GetStorageDirtyRegion: up to maxRects disjoint rects
        // that together cover every dirty texel, so ~100 sprite writes in a big atlas
        // need not be uploaded as one atlas-sized box. Returns how many rects were
        // written to outRects; 0 means "no list, upload the union box" and is always a
        // safe answer - this base fallback keeps whole-level semantics for storage
        // classes that do not track rects, and backends OPT IN by calling this.
        virtual SizeT GetStorageDirtyRects(TextureUploadTarget uploadTarget, Uint mipmapLevel,
                                           MipmapDirtyRegion* outRects, SizeT maxRects) const {
            (void)uploadTarget;
            (void)mipmapLevel;
            (void)outRects;
            (void)maxRects;
            return 0;
        }

        // The compressed image a glCompressedTexImage* call shadowed for this level, kept verbatim
        // next to the texel data rather than instead of it - see MipmapStorage. The texel shadow
        // stays uncompressed and correctly sized, so nothing in the backend upload path has to know
        // these exist; only glGetCompressedTexImage, glGetCompressedTextureImage and the
        // GL_TEXTURE_COMPRESSED* level queries read them.
        virtual void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
                                              GLenum internalFormat, const void* data, SizeT size) = 0;
        // GL_NONE when this level is not stored compressed.
        virtual GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
        virtual SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
        virtual const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;

        // The compressed internalformat the level was REQUESTED with, recorded even when MobileGL
        // answered it with uncompressed storage (the six generic GL_COMPRESSED_* enums) - see
        // MipmapStorage. Only the entry points GL forbids on a compressed image read it.
        virtual void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel,
                                                        GLenum internalFormat) = 0;
        // GL_NONE when the level was not requested with a compressed internalformat.
        virtual GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
                                                          Uint mipmapLevel) const = 0;
    };

    // Cheap replacement for dynamic_cast on the hot path: TextureObjectMipmap is the
    // only hierarchy branch whose storage type reports Mipmap, so the tag check makes
    // the static_cast safe.
    inline TextureObjectMipmap* AsMipmapTexture(ITextureObject* texture) {
        return (texture && texture->GetStorageType() == TextureStorageType::Mipmap)
                   ? static_cast<TextureObjectMipmap*>(texture)
                   : nullptr;
    }
    // Whether the texture satisfies the mipmap-completeness rules a minification filter
    // that samples the mip chain imposes (GL 4.6 core 8.17): every level from the base to
    // the effective max must exist at exactly half the previous one's size. `mipmapped` is
    // the effective sampler's answer to "does this filter read more than the base level" -
    // when it is false only base-level completeness matters, which the ordinary
    // IsComplete() already covers. Sampling an incomplete texture returns (0, 0, 0, 1).
    Bool IsMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped);

    // The rule above asked as the backends need it: does a lookup on this texture read
    // (0, 0, 0, 1) instead of its contents? `effectiveSampler` is the sampler object bound
    // to the unit when there is one, otherwise the texture's own. A backend answers yes by
    // routing the texture to whatever it already uses for "nothing is bound there".
    Bool SamplesAsIncompleteTexture(const ITextureObject* texture, const SamplerObject* effectiveSampler);

    inline const TextureObjectMipmap* AsMipmapTexture(const ITextureObject* texture) {
        return (texture && texture->GetStorageType() == TextureStorageType::Mipmap)
                   ? static_cast<const TextureObjectMipmap*>(texture)
                   : nullptr;
    }

    // The per-target default texture objects (name 0) sit permanently in every texture unit's
    // binding slots, so "nothing useful bound" is no longer a null slot. While a default texture
    // has never been given an image (its internal format is still Unknown) it can contribute
    // nothing to sampling; backends treat such a binding exactly like the old empty slot and
    // skip per-draw sync/bind work for it. Once an application defines an image on a default
    // texture it loses this shortcut and is synced like any other texture.
    inline Bool IsUndefinedDefaultTexture(const ITextureObject* texture) {
        return texture != nullptr && texture->GetExternalIndex() == 0 &&
               texture->GetFormat() == TextureInternalFormat::Unknown;
    }

    class TextureObjectWithOneMipmap : public TextureObjectMipmap {
    public:
        TextureObjectWithOneMipmap(TextureTarget target, Uint externalIndex)
            : TextureObjectMipmap(target, externalIndex) {}
        virtual ~TextureObjectWithOneMipmap() = default;

        Uint GetMipmapLevelCount() const override;
        const IntVec3 GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const override;
        const SizeT GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const override;
        void AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) override;
        void TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) override;
        void UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) override;
        void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override;
        void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty) override;
        bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
        void MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, IntVec3 offset,
                                    IntVec3 size) override;
        MipmapDirtyRegion GetStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
        SizeT GetStorageDirtyRects(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapDirtyRegion* outRects,
                                   SizeT maxRects) const override;
        void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat,
                                      const void* data, SizeT size) override;
        GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
        SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
        const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
        void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel,
                                                GLenum internalFormat) override;
        GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;

        IntVec3 GetBaseSize() const override;
        Bool IsComplete() const override;

    protected:
        MipmapUploadTargetArray<1> m_textureStorage;
    };
} // namespace MobileGL::MG_State::GLState
