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

#include "SkTypes.h"

#include "GrContext.h"
#include "GrContextFactory.h"
#include "GrContextPriv.h"
#include "GrProxyProvider.h"
#include "GrSamplerState.h"
#include "GrTextureProducer.h"
#include "GrTextureProxy.h"
#include "GrTypes.h"
#include "GrTypesPriv.h"
#include "SkRect.h"
#include "SkRefCnt.h"
#include "Test.h"

#include <initializer_list>

// For DetermineDomainMode (in the MDB world) we have 3 rects:
//      1) the final instantiated backing storage (i.e., the actual GrTexture's extent)
//      2) the proxy's extent, which may or may not match the GrTexture's extent
//      3) the constraint rect, which can optionally be hard or soft
// This test "fuzzes" all the combinations of these rects.
class GrTextureProducer_TestAccess {
public:
    using DomainMode = GrTextureProducer::DomainMode;

    static DomainMode DetermineDomainMode(const SkRect& constraintRect,
                                          GrTextureProducer::FilterConstraint filterConstraint,
                                          bool coordsLimitedToConstraintRect,
                                          GrTextureProxy* proxy,
                                          const GrSamplerState::Filter* filterModeOrNullForBicubic,
                                          SkRect* domainRect) {
        return GrTextureProducer::DetermineDomainMode(constraintRect,
                                                      filterConstraint,
                                                      coordsLimitedToConstraintRect,
                                                      proxy,
                                                      filterModeOrNullForBicubic,
                                                      domainRect);
    }
};

using DomainMode = GrTextureProducer_TestAccess::DomainMode;

class RectInfo {
public:
    enum Side { kLeft = 0, kTop = 1, kRight = 2, kBot = 3 };

    enum EdgeType {
        kSoft = 0,   // there is data on the other side of this edge that we are allowed to sample
        kHard = 1,   // the backing resource ends at this edge
        kBad  = 2    // we can't sample across this edge
    };

    void set(const SkRect& rect, EdgeType left, EdgeType top, EdgeType right, EdgeType bot,
             const char* name) {
        fRect = rect;
        fTypes[kLeft]  = left;
        fTypes[kTop]   = top;
        fTypes[kRight] = right;
        fTypes[kBot]   = bot;
        fName = name;
    }

    const SkRect& rect() const { return fRect; }
    EdgeType edgeType(Side side) const { return fTypes[side]; }
    const char* name() const { return fName; }

#ifdef SK_DEBUG
    bool isHardOrBadAllAround() const {
        for (int i = 0; i < 4; ++i) {
            if (kHard != fTypes[i] && kBad != fTypes[i]) {
                return false;
            }
        }
        return true;
    }
#endif

    bool hasABad() const {
        for (int i = 0; i < 4; ++i) {
            if (kBad == fTypes[i]) {
                return true;
            }
        }
        return false;
    }

#ifdef SK_DEBUG
    void print(const char* label) const {
        SkDebugf("%s: %s (%.1f, %.1f, %.1f, %.1f), L: %s T: %s R: %s B: %s\n",
                 label, fName,
                 fRect.fLeft, fRect.fTop, fRect.fRight, fRect.fBottom,
                 ToStr(fTypes[kLeft]), ToStr(fTypes[kTop]),
                 ToStr(fTypes[kRight]), ToStr(fTypes[kBot]));
    }
#endif

private:
#ifdef SK_DEBUG
    static const char* ToStr(EdgeType type) {
        static const char* names[] = { "soft", "hard", "bad" };
        return names[type];
    }
#endif

    RectInfo operator=(const RectInfo& other); // disallow

    SkRect      fRect;
    EdgeType    fTypes[4];
    const char* fName;

};

static sk_sp<GrTextureProxy> create_proxy(GrContext* ctx,
                                          bool isPowerOfTwo,
                                          bool isExact,
                                          RectInfo* rect) {
    GrProxyProvider* proxyProvider = ctx->priv().proxyProvider();
    int size = isPowerOfTwo ? 128 : 100;
    SkBackingFit fit = isExact ? SkBackingFit::kExact : SkBackingFit::kApprox;

    GrSurfaceDesc desc;
    desc.fWidth = size;
    desc.fHeight = size;
    desc.fConfig = kRGBA_8888_GrPixelConfig;

    GrBackendFormat format =
            ctx->priv().caps()->getBackendFormatFromColorType(kRGBA_8888_SkColorType);

    static const char* name = "proxy";

    // Proxies are always hard on the left and top but can be bad on the right and bottom
    rect->set(SkRect::MakeWH(size, size),
              RectInfo::kHard,
              RectInfo::kHard,
              (isPowerOfTwo || isExact) ? RectInfo::kHard : RectInfo::kBad,
              (isPowerOfTwo || isExact) ? RectInfo::kHard : RectInfo::kBad,
              name);

    return proxyProvider->createProxy(format, desc, kTopLeft_GrSurfaceOrigin, fit,
                                      SkBudgeted::kYes);
}

static RectInfo::EdgeType compute_inset_edgetype(RectInfo::EdgeType previous,
                                                 bool isInsetHard, bool coordsAreLimitedToRect,
                                                 float insetAmount, float halfFilterWidth) {
    if (isInsetHard) {
        if (coordsAreLimitedToRect) {
            SkASSERT(halfFilterWidth >= 0.0f);
            if (0.0f == halfFilterWidth) {
                return RectInfo::kSoft;
            }
        }

        if (0.0f == insetAmount && RectInfo::kHard == previous) {
            return RectInfo::kHard;
        }

        return RectInfo::kBad;
    }

    if (RectInfo::kHard == previous) {
        return RectInfo::kHard;
    }

    if (coordsAreLimitedToRect) {
        SkASSERT(halfFilterWidth >= 0.0f);
        if (0.0 == halfFilterWidth || insetAmount > halfFilterWidth) {
            return RectInfo::kSoft;
        }
    }

    return previous;
}

static const int kInsetLeft_Flag  = 0x1;
static const int kInsetTop_Flag   = 0x2;
static const int kInsetRight_Flag = 0x4;
static const int kInsetBot_Flag   = 0x8;

// If 'isInsetHard' is true we can't sample across the inset boundary.
// If 'areCoordsLimitedToRect' is true the client promises to never sample outside the inset.
static const SkRect* generic_inset(const RectInfo& enclosing,
                                   RectInfo* result,
                                   bool isInsetHard,
                                   bool areCoordsLimitedToRect,
                                   float insetAmount,
                                   float halfFilterWidth,
                                   uint32_t flags,
                                   const char* name) {
    SkRect newR = enclosing.rect();

    RectInfo::EdgeType left = enclosing.edgeType(RectInfo::kLeft);
    if (flags & kInsetLeft_Flag) {
        newR.fLeft += insetAmount;
        left = compute_inset_edgetype(left, isInsetHard, areCoordsLimitedToRect,
                                      insetAmount, halfFilterWidth);
    } else {
        left = compute_inset_edgetype(left, isInsetHard, areCoordsLimitedToRect,
                                      0.0f, halfFilterWidth);
    }

    RectInfo::EdgeType top = enclosing.edgeType(RectInfo::kTop);
    if (flags & kInsetTop_Flag) {
        newR.fTop += insetAmount;
        top = compute_inset_edgetype(top, isInsetHard, areCoordsLimitedToRect,
                                     insetAmount, halfFilterWidth);
    } else {
        top = compute_inset_edgetype(top, isInsetHard, areCoordsLimitedToRect,
                                     0.0f, halfFilterWidth);
    }

    RectInfo::EdgeType right = enclosing.edgeType(RectInfo::kRight);
    if (flags & kInsetRight_Flag) {
        newR.fRight -= insetAmount;
        right = compute_inset_edgetype(right, isInsetHard, areCoordsLimitedToRect,
                                       insetAmount, halfFilterWidth);
    } else {
        right = compute_inset_edgetype(right, isInsetHard, areCoordsLimitedToRect,
                                       0.0f, halfFilterWidth);
    }

    RectInfo::EdgeType bot = enclosing.edgeType(RectInfo::kBot);
    if (flags & kInsetBot_Flag) {
        newR.fBottom -= insetAmount;
        bot = compute_inset_edgetype(bot, isInsetHard, areCoordsLimitedToRect,
                                     insetAmount, halfFilterWidth);
    } else {
        bot = compute_inset_edgetype(bot, isInsetHard, areCoordsLimitedToRect,
                                     0.0f, halfFilterWidth);
    }

    result->set(newR, left, top, right, bot, name);
    return &result->rect();
}

// Make a rect that only touches the enclosing rect on the left.
static const SkRect* left_only(const RectInfo& enclosing,
                               RectInfo* result,
                               bool isInsetHard,
                               bool areCoordsLimitedToRect,
                               float insetAmount,
                               float halfFilterWidth) {
    static const char* name = "left";
    return generic_inset(enclosing, result, isInsetHard, areCoordsLimitedToRect,
                         insetAmount, halfFilterWidth,
                         kInsetTop_Flag|kInsetRight_Flag|kInsetBot_Flag, name);
}

// Make a rect that only touches the enclosing rect on the top.
static const SkRect* top_only(const RectInfo& enclosing,
                               RectInfo* result,
                               bool isInsetHard,
                               bool areCoordsLimitedToRect,
                               float insetAmount,
                               float halfFilterWidth) {
    static const char* name = "top";
    return generic_inset(enclosing, result, isInsetHard, areCoordsLimitedToRect,
                         insetAmount, halfFilterWidth,
                         kInsetLeft_Flag|kInsetRight_Flag|kInsetBot_Flag, name);
}

// Make a rect that only touches the enclosing rect on the right.
static const SkRect* right_only(const RectInfo& enclosing,
                                RectInfo* result,
                                bool isInsetHard,
                                bool areCoordsLimitedToRect,
                                float insetAmount,
                                float halfFilterWidth) {
    static const char* name = "right";
    return generic_inset(enclosing, result, isInsetHard, areCoordsLimitedToRect,
                         insetAmount, halfFilterWidth,
                         kInsetLeft_Flag|kInsetTop_Flag|kInsetBot_Flag, name);
}

// Make a rect that only touches the enclosing rect on the bottom.
static const SkRect* bot_only(const RectInfo& enclosing,
                              RectInfo* result,
                              bool isInsetHard,
                              bool areCoordsLimitedToRect,
                              float insetAmount,
                              float halfFilterWidth) {
    static const char* name = "bot";
    return generic_inset(enclosing, result, isInsetHard, areCoordsLimitedToRect,
                         insetAmount, halfFilterWidth,
                         kInsetLeft_Flag|kInsetTop_Flag|kInsetRight_Flag, name);
}

// Make a rect that is inset all around.
static const SkRect* full_inset(const RectInfo& enclosing,
                                RectInfo* result,
                                bool isInsetHard,
                                bool areCoordsLimitedToRect,
                                float insetAmount,
                                float halfFilterWidth) {
    static const char* name = "all";
    return generic_inset(enclosing, result, isInsetHard, areCoordsLimitedToRect,
                         insetAmount, halfFilterWidth,
                         kInsetLeft_Flag|kInsetTop_Flag|kInsetRight_Flag|kInsetBot_Flag, name);
}

// Make a rect with no inset. This is only used for constraint rect creation.
static const SkRect* no_inset(const RectInfo& enclosing,
                              RectInfo* result,
                              bool isInsetHard,
                              bool areCoordsLimitedToRect,
                              float insetAmount,
                              float halfFilterWidth) {
    static const char* name = "none";
    return generic_inset(enclosing, result, isInsetHard, areCoordsLimitedToRect,
                         insetAmount, halfFilterWidth, 0, name);
}

static void proxy_test(skiatest::Reporter* reporter, GrContext* context) {
    GrTextureProducer_TestAccess::DomainMode actualMode, expectedMode;
    SkRect actualDomainRect;

    static const GrSamplerState::Filter gModes[] = {
            GrSamplerState::Filter::kNearest,
            GrSamplerState::Filter::kBilerp,
            GrSamplerState::Filter::kMipMap,
    };

    static const GrSamplerState::Filter* gModePtrs[] = {&gModes[0], &gModes[1], nullptr,
                                                        &gModes[2]};

    static const float gHalfFilterWidth[] = { 0.0f, 0.5f, 1.5f, 10000.0f };

    for (auto isPowerOfTwoSized : { true, false }) {
        for (auto isExact : { true, false }) {
            RectInfo outermost;

            sk_sp<GrTextureProxy> proxy = create_proxy(context, isPowerOfTwoSized,
                                                       isExact, &outermost);
            SkASSERT(outermost.isHardOrBadAllAround());

            for (auto isConstraintRectHard : { true, false }) {
                for (auto areCoordsLimitedToConstraintRect : { true, false }) {
                    for (int filterMode = 0; filterMode < 4; ++filterMode) {
                        for (auto constraintRectMaker : { left_only, top_only, right_only,
                            bot_only, full_inset, no_inset }) {
                            for (auto insetAmt : { 0.25f, 0.75f, 1.25f, 1.75f, 5.0f }) {
                                RectInfo constraintRectStorage;
                                const SkRect* constraintRect = (*constraintRectMaker)(
                                        outermost,
                                        &constraintRectStorage,
                                        isConstraintRectHard,
                                        areCoordsLimitedToConstraintRect,
                                        insetAmt,
                                        gHalfFilterWidth[filterMode]);
                                SkASSERT(constraintRect); // always need one of these
                                SkASSERT(outermost.rect().contains(*constraintRect));

                                actualMode = GrTextureProducer_TestAccess::DetermineDomainMode(
                                        *constraintRect,
                                        isConstraintRectHard
                                            ? GrTextureProducer::kYes_FilterConstraint
                                            : GrTextureProducer::kNo_FilterConstraint,
                                        areCoordsLimitedToConstraintRect,
                                        proxy.get(),
                                        gModePtrs[filterMode],
                                        &actualDomainRect);

                                expectedMode = DomainMode::kNoDomain_DomainMode;
                                if (constraintRectStorage.hasABad()) {
                                    if (3 == filterMode) {
                                        expectedMode = DomainMode::kTightCopy_DomainMode;
                                    } else {
                                        expectedMode = DomainMode::kDomain_DomainMode;
                                    }
                                }

                                REPORTER_ASSERT(reporter, expectedMode == actualMode);
                                // TODO: add a check that the returned domain rect is correct
                            }
                        }
                    }
                }
            }
        }
    }
}

DEF_GPUTEST_FOR_RENDERING_CONTEXTS(DetermineDomainModeTest, reporter, ctxInfo) {
    GrContext* context = ctxInfo.grContext();

    proxy_test(reporter, context);
}