/*
 * Copyright 2012 The Android Open Source Project
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include "include/effects/SkImageFilters.h"

#include "include/core/SkColor.h"
#include "include/core/SkFlattenable.h"
#include "include/core/SkImageFilter.h"
#include "include/core/SkM44.h"
#include "include/core/SkPoint.h"
#include "include/core/SkPoint3.h"
#include "include/core/SkRect.h"
#include "include/core/SkRefCnt.h"
#include "include/core/SkScalar.h"
#include "include/core/SkShader.h"
#include "include/core/SkTypes.h"
#include "include/effects/SkRuntimeEffect.h"
#include "include/private/base/SkCPUTypes.h"
#include "include/private/base/SkFloatingPoint.h"
#include "include/private/base/SkSpan_impl.h"
#include "src/core/SkImageFilterTypes.h"
#include "src/core/SkImageFilter_Base.h"
#include "src/core/SkKnownRuntimeEffects.h"
#include "src/core/SkReadBuffer.h"
#include "src/core/SkRectPriv.h"
#include "src/core/SkWriteBuffer.h"
#include "src/effects/SkEmbossMaskFilter.h"

#include <optional>
#include <utility>

struct SkISize;

namespace {
// The 3D points/vectors used for lighting don't have a great analog for the rest of the image
// filtering system, and don't have any representation for ParameterSpace and LayerSpace. The
// SVG spec is also vague on how to handle managing them. Using the principle of least-surprise,
// the X and Y coordinates are treated as ParameterSpace<SkPoint|Vector> and the Z will be
// scaled by the average of the X and Y scale factors when tranforming to layer space. For uniform
// scaling transforms, this has the desirable behavior of uniformly scaling the Z axis as well.
struct ZValue {
    ZValue() : fZ(0.f) {}
    ZValue(float z) : fZ(z) {}
    operator float() const { return fZ; }

    float fZ;
};
} // anonymous namespace

namespace skif {
template<>
class LayerSpace<ZValue> {
public:
    LayerSpace() = default;
    explicit LayerSpace(ZValue z) : fData(z) {}

    float val() const { return fData.fZ; }

    static LayerSpace<ZValue> Map(const Mapping& mapping, ParameterSpace<ZValue> z) {
        // See comment on ZValue for rationale.
        skif::LayerSpace<skif::Vector> z2d = mapping.paramToLayer(
                skif::ParameterSpace<skif::Vector>({ZValue(z), ZValue(z)}));
        return LayerSpace<ZValue>(SkScalarAve(z2d.x(), z2d.y()));
    }

private:
    ZValue fData;
};
} // namespace skif

namespace {

struct Light {
    enum class Type {
        kDistant,
        kPoint,
        kSpot,
        kLast = kSpot
    };

    Type fType;
    SkColor fLightColor; // All lights

    // Location and direction are decomposed into typed XY and Z for how they are transformed from
    // parameter space to layer space.
    skif::ParameterSpace<SkPoint> fLocationXY; // Spotlight and point lights only
    skif::ParameterSpace<ZValue>  fLocationZ;  //  ""

    skif::ParameterSpace<skif::Vector> fDirectionXY; // Spotlight and distant lights only
    skif::ParameterSpace<ZValue>       fDirectionZ;  //  ""

    // Spotlight only (and unchanged by layer matrix)
    float fFalloffExponent;
    float fCosCutoffAngle;

    static Light Point(SkColor color, const SkPoint3& location) {
        return {Type::kPoint,
                color,
                skif::ParameterSpace<SkPoint>({location.fX, location.fY}),
                skif::ParameterSpace<ZValue>(location.fZ),
                /*directionXY=*/{},
                /*directionZ=*/{},
                /*falloffExponent=*/0.f,
                /*cutoffAngle=*/0.f};
    }

    static Light Distant(SkColor color, const SkPoint3& direction) {
        return {Type::kDistant,
                color,
                /*locationXY=*/{},
                /*locationZ=*/{},
                skif::ParameterSpace<skif::Vector>({direction.fX, direction.fY}),
                skif::ParameterSpace<ZValue>(direction.fZ),
                /*falloffExponent=*/0.f,
                /*cutoffAngle=*/0.f};
    }

    static Light Spot(SkColor color, const SkPoint3& location, const SkPoint3& direction,
                      float falloffExponent, float cosCutoffAngle) {
        return {Type::kSpot,
                color,
                skif::ParameterSpace<SkPoint>({location.fX, location.fY}),
                skif::ParameterSpace<ZValue>(location.fZ),
                skif::ParameterSpace<skif::Vector>({direction.fX, direction.fY}),
                skif::ParameterSpace<ZValue>(direction.fZ),
                falloffExponent,
                cosCutoffAngle};
    }
};

struct Material {
    enum class Type {
        kDiffuse,
        kSpecular,
        kEmbossSpecular,
        kLast = kEmbossSpecular
    };

    Type fType;
    // The base scale factor applied to alpha image to go from [0-1] to [0-depth] before computing
    // surface normals.
    skif::ParameterSpace<ZValue> fSurfaceDepth;

    // Non-geometric
    float fK; // Reflectance coefficient
    float fShininess; // Specular only

    static Material Diffuse(float k, float surfaceDepth) {
        return {Type::kDiffuse, skif::ParameterSpace<ZValue>(surfaceDepth), k, 0.f};
    }

    static Material Specular(float k, float shininess, float surfaceDepth) {
        return {Type::kSpecular, skif::ParameterSpace<ZValue>(surfaceDepth), k, shininess};
    }

    static Material EmbossSpecular(float k, float shininess, float surfaceDepth) {
        return {Type::kEmbossSpecular , skif::ParameterSpace<ZValue>(surfaceDepth), k, shininess};
    }
};

class SkLightingImageFilter final : public SkImageFilter_Base {
public:
    SkLightingImageFilter(const Light& light, const Material& material, sk_sp<SkImageFilter> input)
            : SkImageFilter_Base(&input, 1)
            , fLight(light)
            , fMaterial(material) {}

    SkRect computeFastBounds(const SkRect& src) const override;

protected:
    void flatten(SkWriteBuffer&) const override;

private:
    friend void ::SkRegisterLightingImageFilterFlattenables();
    SK_FLATTENABLE_HOOKS(SkLightingImageFilter)
    static Light LegacyDeserializeLight(SkReadBuffer& buffer);
    static sk_sp<SkFlattenable> LegacyDiffuseCreateProc(SkReadBuffer& buffer);
    static sk_sp<SkFlattenable> LegacySpecularCreateProc(SkReadBuffer& buffer);

    bool onAffectsTransparentBlack() const override { return true; }

    skif::FilterResult onFilterImage(const skif::Context&) const override;

    skif::LayerSpace<SkIRect> onGetInputLayerBounds(
            const skif::Mapping& mapping,
            const skif::LayerSpace<SkIRect>& desiredOutput,
            std::optional<skif::LayerSpace<SkIRect>> contentBounds) const override;

    std::optional<skif::LayerSpace<SkIRect>> onGetOutputLayerBounds(
            const skif::Mapping& mapping,
            std::optional<skif::LayerSpace<SkIRect>> contentBounds) const override;

    skif::LayerSpace<SkIRect> requiredInput(const skif::LayerSpace<SkIRect>& desiredOutput) const {
        // We request 1px of padding so that the visible normal map can do a regular Sobel kernel
        // eval. The Sobel kernel is always applied in layer pixels
        skif::LayerSpace<SkIRect> requiredInput = desiredOutput;
        requiredInput.outset(skif::LayerSpace<SkISize>({1, 1}));
        return requiredInput;
    }

    Light fLight;
    Material fMaterial;
};

// Creates a shader that performs a Sobel filter on the alpha channel of the input image, using
// 'edgeBounds' to decide how to modify the kernel weights.
sk_sp<SkShader> make_normal_shader(sk_sp<SkShader> alphaMap,
                                   const skif::LayerSpace<SkIRect>& edgeBounds,
                                   skif::LayerSpace<ZValue> surfaceDepth) {
    const SkRuntimeEffect* normalEffect =
            GetKnownRuntimeEffect(SkKnownRuntimeEffects::StableKey::kNormal);

    SkRuntimeShaderBuilder builder(sk_ref_sp(normalEffect));
    builder.child("alphaMap") = std::move(alphaMap);
    builder.uniform("edgeBounds") = SkRect::Make(SkIRect(edgeBounds)).makeInset(0.5f, 0.5f);
    builder.uniform("negSurfaceDepth") = -surfaceDepth.val();

    return builder.makeShader();
}

sk_sp<SkShader> make_lighting_shader(sk_sp<SkShader> normalMap,
                                     Light::Type lightType,
                                     SkColor lightColor,
                                     skif::LayerSpace<SkPoint> locationXY,
                                     skif::LayerSpace<ZValue> locationZ,
                                     skif::LayerSpace<skif::Vector> directionXY,
                                     skif::LayerSpace<ZValue> directionZ,
                                     float falloffExponent,
                                     float cosCutoffAngle,
                                     Material::Type matType,
                                     skif::LayerSpace<ZValue> surfaceDepth,
                                     float k,
                                     float shininess) {

    const SkRuntimeEffect* lightingEffect =
            GetKnownRuntimeEffect(SkKnownRuntimeEffects::StableKey::kLighting);

    SkRuntimeShaderBuilder builder(sk_ref_sp(lightingEffect));
    builder.child("normalMap") = std::move(normalMap);

    builder.uniform("materialAndLightType") =
            SkV4{surfaceDepth.val(),
                 shininess,
                 static_cast<float>(matType),
                 lightType == Light::Type::kPoint ?
                         0.f : (lightType == Light::Type::kDistant ? -1.f : 1.f)};
    builder.uniform("lightPosAndSpotFalloff") =
            SkV4{locationXY.x(), locationXY.y(), locationZ.val(), falloffExponent};

    // Pre-normalize the light direction, but this can be (0,0,0) for point lights, which won't use
    // the uniform anyways. Avoid a division by 0 to keep ASAN happy or in the event that a spot/dir
    // light have bad user input.
    SkV3 dir{directionXY.x(), directionXY.y(), directionZ.val()};
    float invDirLen = dir.length();
    invDirLen = invDirLen ? 1.0f / invDirLen : 0.f;
    builder.uniform("lightDirAndSpotCutoff") =
            SkV4{invDirLen*dir.x, invDirLen*dir.y, invDirLen*dir.z, cosCutoffAngle};

    // Historically, the Skia lighting image filter did not apply any color space transformation to
    // the light's color. The SVG spec for the lighting effects does not stipulate how to interpret
    // the color for a light. Overall, it does not have a principled physically based approach, but
    // the closest way to interpret it, is:
    //  - the material's K is a uniformly distributed reflectance coefficient
    //  - lighting *should* be calculated in a linear color space, which is the default for SVG
    //    filters. Chromium manages these color transformations using SkImageFilters::ColorFilter
    //    so it's not necessarily reflected in the Context's color space.
    //  - it's unspecified in the SVG spec if the light color should be transformed to linear or
    //    interpreted as linear already. Regardless, if there was any transformation that needed to
    //    occur, Blink took care of it in the past so adding color space management to the light
    //    color would be a breaking change.
    //  - so for now, leave the color un-modified and apply K up front since no color space
    //    transforms need to be performed on the original light color.
    const float colorScale = k / 255.f;
    builder.uniform("lightColor") = SkV3{SkColorGetR(lightColor) * colorScale,
                                         SkColorGetG(lightColor) * colorScale,
                                         SkColorGetB(lightColor) * colorScale};

    return builder.makeShader();
}

sk_sp<SkImageFilter> make_lighting(const Light& light,
                                   const Material& material,
                                   sk_sp<SkImageFilter> input,
                                   const SkImageFilters::CropRect& cropRect) {
    // According to the spec, ks and kd can be any non-negative number:
    // http://www.w3.org/TR/SVG/filters.html#feSpecularLightingElement
    if (!SkIsFinite(material.fK, material.fShininess, ZValue(material.fSurfaceDepth)) ||
        material.fK < 0.f) {
        return nullptr;
    }

    // Ensure light values are finite, and the cosine should be between -1 and 1
    if (!SkPoint(light.fLocationXY).isFinite() || !skif::Vector(light.fDirectionXY).isFinite() ||
        !SkIsFinite(light.fFalloffExponent, light.fCosCutoffAngle,
                    ZValue(light.fLocationZ), ZValue(light.fDirectionZ)) ||
        light.fCosCutoffAngle < -1.f || light.fCosCutoffAngle > 1.f) {
        return nullptr;
    }

    // If a crop rect is provided, it clamps both the input (to better match the SVG's normal
    // boundary condition spec) and the output (because otherwise it has infinite bounds).
    sk_sp<SkImageFilter> filter = std::move(input);
    if (cropRect) {
        filter = SkImageFilters::Crop(*cropRect, std::move(filter));
    }
    filter = sk_sp<SkImageFilter>(
            new SkLightingImageFilter(light, material, std::move(filter)));
    if (cropRect) {
        filter = SkImageFilters::Crop(*cropRect, std::move(filter));
    }
    return filter;
}

} // anonymous namespace

sk_sp<SkImageFilter> SkImageFilters::DistantLitDiffuse(
        const SkPoint3& direction, SkColor lightColor, SkScalar surfaceScale, SkScalar kd,
        sk_sp<SkImageFilter> input, const CropRect& cropRect) {
    return make_lighting(Light::Distant(lightColor, direction),
                         Material::Diffuse(kd, surfaceScale),
                         std::move(input), cropRect);
}

sk_sp<SkImageFilter> SkImageFilters::PointLitDiffuse(
        const SkPoint3& location, SkColor lightColor, SkScalar surfaceScale, SkScalar kd,
        sk_sp<SkImageFilter> input, const CropRect& cropRect) {
    return make_lighting(Light::Point(lightColor, location),
                         Material::Diffuse(kd, surfaceScale),
                         std::move(input), cropRect);
}

sk_sp<SkImageFilter> SkImageFilters::SpotLitDiffuse(
        const SkPoint3& location, const SkPoint3& target, SkScalar falloffExponent,
        SkScalar cutoffAngle, SkColor lightColor, SkScalar surfaceScale, SkScalar kd,
        sk_sp<SkImageFilter> input, const CropRect& cropRect) {
    SkPoint3 dir = target - location;
    float cosCutoffAngle = SkScalarCos(SkDegreesToRadians(cutoffAngle));
    return make_lighting(Light::Spot(lightColor, location, dir, falloffExponent, cosCutoffAngle),
                         Material::Diffuse(kd, surfaceScale),
                         std::move(input), cropRect);
}

// Private factory method for usage in SkEmbossMaskFilter.
sk_sp<SkImageFilter> SkEmbossMaskFilter::LegacySpecular(
        const SkPoint3& direction, SkColor lightColor, SkScalar surfaceScale, SkScalar ks,
        SkScalar shininess, sk_sp<SkImageFilter> input) {
    return make_lighting(::Light::Distant(lightColor, direction),
                         Material::EmbossSpecular(ks, shininess, surfaceScale),
                         std::move(input), {});
}

sk_sp<SkImageFilter> SkImageFilters::DistantLitSpecular(
        const SkPoint3& direction, SkColor lightColor, SkScalar surfaceScale, SkScalar ks,
        SkScalar shininess, sk_sp<SkImageFilter> input, const CropRect& cropRect) {
    return make_lighting(Light::Distant(lightColor, direction),
                         Material::Specular(ks, shininess, surfaceScale),
                         std::move(input), cropRect);
}

sk_sp<SkImageFilter> SkImageFilters::PointLitSpecular(
        const SkPoint3& location, SkColor lightColor, SkScalar surfaceScale, SkScalar ks,
        SkScalar shininess, sk_sp<SkImageFilter> input, const CropRect& cropRect) {
    return make_lighting(Light::Point(lightColor, location),
                         Material::Specular(ks, shininess, surfaceScale),
                         std::move(input), cropRect);
}

sk_sp<SkImageFilter> SkImageFilters::SpotLitSpecular(
        const SkPoint3& location, const SkPoint3& target, SkScalar falloffExponent,
        SkScalar cutoffAngle, SkColor lightColor, SkScalar surfaceScale, SkScalar ks,
        SkScalar shininess, sk_sp<SkImageFilter> input, const CropRect& cropRect) {
    SkPoint3 dir = target - location;
    float cosCutoffAngle = SkScalarCos(SkDegreesToRadians(cutoffAngle));
    return make_lighting(Light::Spot(lightColor, location, dir, falloffExponent, cosCutoffAngle),
                         Material::Specular(ks, shininess, surfaceScale),
                         std::move(input), cropRect);
}

void SkRegisterLightingImageFilterFlattenables() {
    SK_REGISTER_FLATTENABLE(SkLightingImageFilter);
    // TODO (michaelludwig): Remove after grace period for SKPs to stop using old name
    SkFlattenable::Register("SkDiffuseLightingImageFilter",
                            SkLightingImageFilter::LegacyDiffuseCreateProc);
    SkFlattenable::Register("SkSpecularLightingImageFilter",
                            SkLightingImageFilter::LegacySpecularCreateProc);
}

///////////////////////////////////////////////////////////////////////////////

sk_sp<SkFlattenable> SkLightingImageFilter::CreateProc(SkReadBuffer& buffer) {
    SK_IMAGEFILTER_UNFLATTEN_COMMON(common, 1);

    Light light;
    light.fType = buffer.read32LE(Light::Type::kLast);
    light.fLightColor = buffer.readColor();

    SkPoint3 lightPos, lightDir;
    buffer.readPoint3(&lightPos);
    light.fLocationXY = skif::ParameterSpace<SkPoint>({lightPos.fX, lightPos.fY});
    light.fLocationZ = skif::ParameterSpace<ZValue>(lightPos.fZ);

    buffer.readPoint3(&lightDir);
    light.fDirectionXY = skif::ParameterSpace<skif::Vector>({lightDir.fX, lightDir.fY});
    light.fDirectionZ = skif::ParameterSpace<ZValue>(lightDir.fZ);

    light.fFalloffExponent = buffer.readScalar();
    light.fCosCutoffAngle = buffer.readScalar();

    Material material;
    material.fType = buffer.read32LE(Material::Type::kLast);
    material.fSurfaceDepth = skif::ParameterSpace<ZValue>(buffer.readScalar());
    material.fK = buffer.readScalar();
    material.fShininess = buffer.readScalar();

    if (!buffer.isValid()) {
        return nullptr;
    }

    return make_lighting(light, material, common.getInput(0), common.cropRect());
}

Light SkLightingImageFilter::LegacyDeserializeLight(SkReadBuffer& buffer) {
    // Light::Type has the same order as the legacy SkImageFilterLight::LightType enum
    Light::Type lightType = buffer.read32LE(Light::Type::kLast);
    if (!buffer.isValid()) {
        return {};
    }

    // Legacy lights stored just the RGB, but as floats (notably *not* normalized to [0-1])
    SkColor lightColor = SkColorSetARGB(/*a (ignored)=*/255,
                                        /*r=*/ (U8CPU) buffer.readScalar(),
                                        /*g=*/ (U8CPU) buffer.readScalar(),
                                        /*b=*/ (U8CPU) buffer.readScalar());
    // Legacy lights only serialized fields specific to that type
    switch (lightType) {
        case Light::Type::kDistant: {
            SkPoint3 dir = {buffer.readScalar(), buffer.readScalar(), buffer.readScalar()};
            return Light::Distant(lightColor, dir);
        }
        case Light::Type::kPoint: {
            SkPoint3 loc = {buffer.readScalar(), buffer.readScalar(), buffer.readScalar()};
            return Light::Point(lightColor, loc);
        }
        case Light::Type::kSpot: {
            SkPoint3 loc = {buffer.readScalar(), buffer.readScalar(), buffer.readScalar()};
            SkPoint3 target = {buffer.readScalar(), buffer.readScalar(), buffer.readScalar()};
            float falloffExponent = buffer.readScalar();
            float cosOuterConeAngle = buffer.readScalar();
            buffer.readScalar(); // skip cosInnerConeAngle, derived from outer cone angle
            buffer.readScalar(); // skip coneScale, which is a constant
            buffer.readScalar(); // skip S, which is normalize(target - loc)
            buffer.readScalar(); //  ""
            buffer.readScalar(); //  ""
            return Light::Spot(lightColor, loc, target - loc, falloffExponent, cosOuterConeAngle);
        }
    }

    SkUNREACHABLE; // Validation by read32LE() should avoid this
}

sk_sp<SkFlattenable> SkLightingImageFilter::LegacyDiffuseCreateProc(SkReadBuffer& buffer) {
    SK_IMAGEFILTER_UNFLATTEN_COMMON(common, 1);

    Light light = LegacyDeserializeLight(buffer);

    // Legacy implementations used (scale/255) when filtering, but serialized (fScale*255) so the
    // buffer held the original unmodified surface scale.
    float surfaceScale = buffer.readScalar();
    float kd = buffer.readScalar();
    Material material = Material::Diffuse(kd, surfaceScale);

    return make_lighting(light, material, common.getInput(0), common.cropRect());
}

sk_sp<SkFlattenable> SkLightingImageFilter::LegacySpecularCreateProc(SkReadBuffer& buffer) {
    SK_IMAGEFILTER_UNFLATTEN_COMMON(common, 1);

    Light light = LegacyDeserializeLight(buffer);

    // Legacy implementations used (scale/255) when filtering, but serialized (fScale*255) so the
    // buffer held the original unmodified surface scale.
    float surfaceScale = buffer.readScalar();
    float ks = buffer.readScalar();
    float shininess = buffer.readScalar();
    Material material = Material::Specular(ks, shininess, surfaceScale);

    return make_lighting(light, material, common.getInput(0), common.cropRect());
}

void SkLightingImageFilter::flatten(SkWriteBuffer& buffer) const {
    this->SkImageFilter_Base::flatten(buffer);

    // Light
    buffer.writeInt((int) fLight.fType);
    buffer.writeColor(fLight.fLightColor);

    buffer.writePoint(SkPoint(fLight.fLocationXY));
    buffer.writeScalar(ZValue(fLight.fLocationZ));

    skif::Vector dirXY{fLight.fDirectionXY};
    buffer.writePoint(SkPoint{dirXY.fX, dirXY.fY});
    buffer.writeScalar(ZValue(fLight.fDirectionZ));

    buffer.writeScalar(fLight.fFalloffExponent);
    buffer.writeScalar(fLight.fCosCutoffAngle);

    // Material
    buffer.writeInt((int) fMaterial.fType);
    buffer.writeScalar(ZValue(fMaterial.fSurfaceDepth));
    buffer.writeScalar(fMaterial.fK);
    buffer.writeScalar(fMaterial.fShininess);
}

///////////////////////////////////////////////////////////////////////////////

skif::FilterResult SkLightingImageFilter::onFilterImage(const skif::Context& ctx) const {
    using ShaderFlags = skif::FilterResult::ShaderFlags;

    auto mapZToLayer = [&ctx](skif::ParameterSpace<ZValue> z) {
        return skif::LayerSpace<ZValue>::Map(ctx.mapping(), z);
    };

    // Map lighting and material parameters into layer space
    skif::LayerSpace<ZValue> surfaceDepth = mapZToLayer(fMaterial.fSurfaceDepth);
    skif::LayerSpace<SkPoint> lightLocationXY = ctx.mapping().paramToLayer(fLight.fLocationXY);
    skif::LayerSpace<ZValue> lightLocationZ = mapZToLayer(fLight.fLocationZ);
    skif::LayerSpace<skif::Vector> lightDirXY = ctx.mapping().paramToLayer(fLight.fDirectionXY);
    skif::LayerSpace<ZValue> lightDirZ = mapZToLayer(fLight.fDirectionZ);

    // The normal map is determined by a 3x3 kernel, so we request a 1px outset of what should be
    // filled by the lighting equation. Ideally this means there are no boundary conditions visible.
    // If the required input is incomplete, the lighting filter handles the boundaries in two ways:
    // - When the actual child output's edge matches the desired output's edge, it uses clamped
    //   tiling at the desired output. This approximates the modified Sobel kernel's specified in
    //   https://drafts.fxtf.org/filter-effects/#feDiffuseLightingElement. NOTE: It's identical to
    //   the interior kernel and near equal on the 4 edges (only weights are biased differently).
    //   The four corners' convolution sums with clamped tiling are not equal, but should not be
    //   objectionable since the normals produced are reasonable and still further processed by the
    //   lighting equation. The increased complexity is not worth it for just 4 pixels of output.
    // - However, when the desired output is far larger than the produced image, we process the
    //   child output with the default decal tiling that the Skia image filter pipeline relies on.
    //   This creates a visual bevel at the image boundary but avoids producing streaked normals if
    //   the clamped tiling was used in all scenarios.
    skif::LayerSpace<SkIRect> requiredInput = this->requiredInput(ctx.desiredOutput());
    skif::FilterResult childOutput =
            this->getChildOutput(0, ctx.withNewDesiredOutput(requiredInput));

    skif::LayerSpace<SkIRect> clampRect = requiredInput; // effectively no clamping of normals
    if (!childOutput.layerBounds().contains(requiredInput)) {
        // Adjust clampRect edges to desiredOutput if the actual child output matched the lighting
        // output size (typical SVG case). Otherwise leave coordinates alone to use decal tiling
        // automatically for the pixels outside the child image but inside the desired output.
        auto edgeClamp = [](int actualEdgeValue, int requestedEdgeValue, int outputEdge) {
            return actualEdgeValue == outputEdge ? outputEdge : requestedEdgeValue;
        };
        auto inputRect = childOutput.layerBounds();
        auto clampTo = ctx.desiredOutput();
        clampRect = skif::LayerSpace<SkIRect>({
                edgeClamp(inputRect.left(),   requiredInput.left(),   clampTo.left()),
                edgeClamp(inputRect.top(),    requiredInput.top(),    clampTo.top()),
                edgeClamp(inputRect.right(),  requiredInput.right(),  clampTo.right()),
                edgeClamp(inputRect.bottom(), requiredInput.bottom(), clampTo.bottom())});
    }

    skif::FilterResult::Builder builder{ctx};
    builder.add(childOutput, /*sampleBounds=*/clampRect, ShaderFlags::kSampledRepeatedly);
    return builder.eval([&](SkSpan<sk_sp<SkShader>> input) {
        // TODO: Once shaders are deferred in FilterResult, it will likely make sense to have an
        // internal normal map filter that uses this shader, and then have the lighting effects as
        // a separate filter. It's common for multiple lights to use the same input (producing the
        // same normal map) before being merged together. With a separate normal image filter, its
        // output would be automatically cached, and the lighting equation shader would be deferred
        // to the merge's draw operation, making for a maximum of 2 renderpasses instead of N+1.
        sk_sp<SkShader> normals = make_normal_shader(std::move(input[0]), clampRect, surfaceDepth);
        return make_lighting_shader(std::move(normals),
                                    // Light in layer space
                                    fLight.fType,
                                    fLight.fLightColor,
                                    lightLocationXY,
                                    lightLocationZ,
                                    lightDirXY,
                                    lightDirZ,
                                    fLight.fFalloffExponent,
                                    fLight.fCosCutoffAngle,
                                    // Material in layer space
                                    fMaterial.fType,
                                    surfaceDepth,
                                    fMaterial.fK,
                                    fMaterial.fShininess);
    });
}

skif::LayerSpace<SkIRect> SkLightingImageFilter::onGetInputLayerBounds(
        const skif::Mapping& mapping,
        const skif::LayerSpace<SkIRect>& desiredOutput,
        std::optional<skif::LayerSpace<SkIRect>> contentBounds) const {
    skif::LayerSpace<SkIRect> requiredInput = this->requiredInput(desiredOutput);
    return this->getChildInputLayerBounds(0, mapping, requiredInput, contentBounds);
}

std::optional<skif::LayerSpace<SkIRect>> SkLightingImageFilter::onGetOutputLayerBounds(
        const skif::Mapping& mapping,
        std::optional<skif::LayerSpace<SkIRect>> contentBounds) const {
    // The lighting equation is defined on the entire plane, even if the input image that defines
    // the normal map is bounded. It just is evaluated at a constant normal vector, which can still
    // produce non-constant color since the direction to the eye and light change per pixel.
    return skif::LayerSpace<SkIRect>::Unbounded();
}

SkRect SkLightingImageFilter::computeFastBounds(const SkRect& src) const {
    return SkRectPriv::MakeLargeS32();
}
