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

#include "gm/gm.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkData.h"
#include "include/core/SkPaint.h"
#include "include/core/SkSize.h"
#include "include/core/SkString.h"
#include "include/core/SkSurface.h"
#include "include/effects/SkRuntimeEffect.h"
#include "tools/Resources.h"

class KawaseBlurFilter {
public:
    // Downsample scale factor used to improve performance
    static constexpr float kInputScale = 0.25f;
    // Downsample scale factor used to improve performance
    static constexpr float kInverseInputScale = 1.0f / kInputScale;
    // Maximum number of render passes
    static constexpr uint32_t kMaxPasses = 4;
    // To avoid downscaling artifacts, we interpolate the blurred fbo with the full composited
    // image, up to this radius.
    static constexpr float kMaxCrossFadeRadius = 30.0f;

    KawaseBlurFilter() {
        SkString blurString(R"(
            uniform shader src;
            uniform float in_inverseScale;
            uniform float2 in_blurOffset;

            half4 main(float2 xy) {
                float2 scaled_xy = float2(xy.x * in_inverseScale, xy.y * in_inverseScale);

                half4 c = src.eval(scaled_xy);
                c += src.eval(scaled_xy + float2( in_blurOffset.x,  in_blurOffset.y));
                c += src.eval(scaled_xy + float2( in_blurOffset.x, -in_blurOffset.y));
                c += src.eval(scaled_xy + float2(-in_blurOffset.x,  in_blurOffset.y));
                c += src.eval(scaled_xy + float2(-in_blurOffset.x, -in_blurOffset.y));

                return half4(c.rgb * 0.2, 1.0);
            }
        )");

        SkString mixString(R"(
            uniform shader in_blur;
            uniform shader in_original;
            uniform float in_inverseScale;
            uniform float in_mix;

            half4 main(float2 xy) {
                float2 scaled_xy = float2(xy.x * in_inverseScale, xy.y * in_inverseScale);

                half4 blurred = in_blur.eval(scaled_xy);
                half4 composition = in_original.eval(xy);
                return mix(composition, blurred, in_mix);
            }
        )");

        auto [blurEffect, error] = SkRuntimeEffect::MakeForShader(blurString);
        if (!blurEffect) {
            SkDEBUGFAILF("RuntimeShader error: %s\n", error.c_str());
        }
        fBlurEffect = std::move(blurEffect);

        auto [mixEffect, error2] = SkRuntimeEffect::MakeForShader(mixString);
        if (!mixEffect) {
            SkDEBUGFAILF("RuntimeShader error: %s\n", error2.c_str());
        }
        fMixEffect = std::move(mixEffect);
    }

    static sk_sp<SkSurface> MakeSurface(SkCanvas* canvas, const SkImageInfo& info) {
        if (sk_sp<SkSurface> surface = canvas->makeSurface(info)) {
            return surface;
        }
        // serialize-8888 returns null from makeSurface; fallback to a raster surface.
        return SkSurfaces::Raster(info);
    }

    void draw(SkCanvas* canvas, sk_sp<SkImage> input, int blurRadius) {
        // NOTE: this is only experimental and the current blur params cause points to be sampled
        // beyond the input blur radius.

        // Kawase is an approximation of Gaussian, but it behaves differently from it.
        // A radius transformation is required for approximating them, and also to introduce
        // non-integer steps, necessary to smoothly interpolate large radii.
        float tmpRadius = (float)blurRadius / 6.0f;
        float numberOfPasses = std::min(kMaxPasses, (uint32_t)ceil(tmpRadius));
        float radiusByPasses = tmpRadius / (float)numberOfPasses;

        SkImageInfo scaledInfo = SkImageInfo::MakeN32Premul((float)input->width() * kInputScale,
                                                            (float)input->height() * kInputScale);
        auto drawSurface = MakeSurface(canvas, scaledInfo);

        const float stepX = radiusByPasses;
        const float stepY = radiusByPasses;

        // start by drawing and downscaling and doing the first blur pass
        SkRuntimeShaderBuilder blurBuilder(fBlurEffect);
        blurBuilder.child("src") = input->makeShader(SkSamplingOptions(SkFilterMode::kLinear));
        blurBuilder.uniform("in_inverseScale") = kInverseInputScale;
        blurBuilder.uniform("in_blurOffset") = SkV2{stepX * kInverseInputScale,
                                                    stepY * kInverseInputScale};
        SkPaint paint;
        paint.setShader(blurBuilder.makeShader());
        drawSurface->getCanvas()->drawIRect(scaledInfo.bounds(), paint);

        // DEBUG draw each of the stages
        canvas->save();
        canvas->drawImage(drawSurface->makeImageSnapshot(), input->width() / 4, 0,
                          SkSamplingOptions());
        canvas->translate(input->width() / 4, input->height() * 0.75);

        // And now we'll ping pong between our surfaces, to accumulate the result of various
        // offsets.
        auto lastDrawTarget = drawSurface;
        if (numberOfPasses > 1) {
            auto readSurface = drawSurface;
            drawSurface = MakeSurface(canvas, scaledInfo);

            for (auto i = 1; i < numberOfPasses; i++) {
                const float stepScale = (float)i * kInputScale;

                blurBuilder.child("src") = readSurface->makeImageSnapshot()->makeShader(
                        SkSamplingOptions(SkFilterMode::kLinear));
                blurBuilder.uniform("in_inverseScale") = 1.0f;
                blurBuilder.uniform("in_blurOffset") = SkV2{stepX * stepScale , stepY * stepScale};

                paint.setShader(blurBuilder.makeShader());
                drawSurface->getCanvas()->drawIRect(scaledInfo.bounds(), paint);

                // DEBUG draw each of the stages
                canvas->drawImage(drawSurface->makeImageSnapshot(), 0, 0, SkSamplingOptions());
                canvas->translate(0, input->height() * 0.75);

                // Swap buffers for next iteration
                auto tmp = drawSurface;
                drawSurface = readSurface;
                readSurface = tmp;
            }
            lastDrawTarget = readSurface;
        }

        // restore translations done for debug and offset
        canvas->restore();
        SkAutoCanvasRestore acr(canvas, true);
        canvas->translate(input->width(), 0);

        // do the final composition and when we scale our blur up. It will be interpolated
        // with the larger composited texture to hide downscaling artifacts.
        SkRuntimeShaderBuilder mixBuilder(fMixEffect);
        mixBuilder.child("in_blur") = lastDrawTarget->makeImageSnapshot()->makeShader(
                SkSamplingOptions(SkFilterMode::kLinear));
        mixBuilder.child("in_original") =
                input->makeShader(SkSamplingOptions(SkFilterMode::kLinear));
        mixBuilder.uniform("in_inverseScale") = kInputScale;
        mixBuilder.uniform("in_mix") = std::min(1.0f, (float)blurRadius / kMaxCrossFadeRadius);

        paint.setShader(mixBuilder.makeShader());
        canvas->drawIRect(input->bounds(), paint);
    }

private:
    sk_sp<SkRuntimeEffect> fBlurEffect;
    sk_sp<SkRuntimeEffect> fMixEffect;
};

class KawaseBlurRT : public skiagm::GM {
public:
    KawaseBlurRT() {}
    SkString onShortName() override { return SkString("kawase_blur_rt"); }
    SkISize onISize() override { return {1280, 768}; }

    void onOnceBeforeDraw() override {
        fMandrill = GetResourceAsImage("images/mandrill_256.png");
    }

    void onDraw(SkCanvas* canvas) override {
        canvas->drawImage(fMandrill, 0, 0);
        canvas->translate(256, 0);
        KawaseBlurFilter blurFilter;
        blurFilter.draw(canvas, fMandrill, 45);
        canvas->translate(512, 0);
        blurFilter.draw(canvas, fMandrill, 55);
    }

private:
    sk_sp<SkImage> fMandrill;
};
DEF_GM(return new KawaseBlurRT;)
