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

#include "include/core/SkBlendMode.h"
#include "include/core/SkColor.h"
#include "include/core/SkColorFilter.h"
#include "include/core/SkImageFilter.h"
#include "include/core/SkM44.h"
#include "include/core/SkRefCnt.h"
#include "include/core/SkScalar.h"
#include "include/effects/SkColorMatrix.h"
#include "include/effects/SkImageFilters.h"
#include "include/private/base/SkTPin.h"
#include "modules/skottie/src/Adapter.h"
#include "modules/skottie/src/SkottiePriv.h"
#include "modules/skottie/src/SkottieValue.h"
#include "modules/skottie/src/effects/Effects.h"
#include "modules/sksg/include/SkSGRenderEffect.h"
#include "modules/sksg/include/SkSGRenderNode.h"
#include "src/utils/SkJSON.h"

#include <utility>

namespace skottie::internal {

namespace {

class ShadowAdapter final : public DiscardableAdapterBase<ShadowAdapter,
                                                          sksg::ExternalImageFilter> {
public:
    enum Type {
        kDropShadow,
        kInnerShadow,
    };

    ShadowAdapter(const skjson::ObjectValue& jstyle,
                  const AnimationBuilder& abuilder,
                  Type type)
        : fType(type) {
        this->bind(abuilder, jstyle["c"], fColor);
        this->bind(abuilder, jstyle["o"], fOpacity);
        this->bind(abuilder, jstyle["a"], fAngle);
        this->bind(abuilder, jstyle["s"], fSize);
        this->bind(abuilder, jstyle["d"], fDistance);
    }

private:
    void onSync() override {
        const auto    rad = SkDegreesToRadians(180 + fAngle), // 0deg -> left (style)
                    sigma = fSize * kBlurSizeToSigma,
                  opacity = SkTPin(fOpacity / 100, 0.0f, 1.0f);
        const auto  color = static_cast<SkColor4f>(fColor);
        const auto offset = SkV2{ fDistance * SkScalarCos(rad),
                                 -fDistance * SkScalarSin(rad)};

        // Shadow effects largely follow the feDropShadow spec [1]:
        //
        //   1) isolate source alpha
        //   2) apply a gaussian blur
        //   3) apply an offset
        //   4) modulate with a flood/color generator
        //   5) composite with the source
        //
        // Note: as an optimization, we can fold #1 and #4 into a single color matrix filter.
        //
        // Inner shadow differences:
        //
        //   a) operates on the inverse of source alpha
        //   b) the result is masked against the source
        //   c) composited on top of source
        //
        // [1] https://drafts.fxtf.org/filter-effects/#feDropShadowElement

        // Select and colorize the source alpha channel.
        SkColorMatrix cm{0, 0, 0,                  0, color.fR,
                         0, 0, 0,                  0, color.fG,
                         0, 0, 0,                  0, color.fB,
                         0, 0, 0, opacity * color.fA,        0};

        // Inner shadows use the alpha inverse.
        if (fType == Type::kInnerShadow) {
            cm.preConcat({1, 0, 0, 0, 0,
                          0, 1, 0, 0, 0,
                          0, 0, 1, 0, 0,
                          0, 0, 0,-1, 1});
        }
        auto f = SkImageFilters::ColorFilter(SkColorFilters::Matrix(cm), nullptr);

        if (sigma > 0) {
            f = SkImageFilters::Blur(sigma, sigma, std::move(f));
        }

        if (!SkScalarNearlyZero(offset.x) || !SkScalarNearlyZero(offset.y)) {
            f = SkImageFilters::Offset(offset.x, offset.y, std::move(f));
        }

        sk_sp<SkImageFilter> source;

        if (fType == Type::kInnerShadow) {
            // Inner shadows draw on top of, and are masked with, the source.
            f = SkImageFilters::Blend(SkBlendMode::kDstIn, std::move(f));

            std::swap(source, f);
        }

        this->node()->setImageFilter(SkImageFilters::Merge(std::move(f),
                                                           std::move(source)));
    }

    const Type fType;

    ColorValue  fColor;
    ScalarValue fOpacity  = 100, // percentage
                fAngle    =   0, // degrees
                fSize     =   0,
                fDistance =   0;

    using INHERITED = DiscardableAdapterBase<ShadowAdapter, sksg::ExternalImageFilter>;
};

static sk_sp<sksg::RenderNode> make_shadow_effect(const skjson::ObjectValue& jstyle,
                                                  const AnimationBuilder& abuilder,
                                                  sk_sp<sksg::RenderNode> layer,
                                                  ShadowAdapter::Type type) {
    auto filter_node = abuilder.attachDiscardableAdapter<ShadowAdapter>(jstyle, abuilder, type);

    return sksg::ImageFilterEffect::Make(std::move(layer), std::move(filter_node));
}

} // namespace

sk_sp<sksg::RenderNode> EffectBuilder::attachDropShadowStyle(const skjson::ObjectValue& jstyle,
                                                             sk_sp<sksg::RenderNode> layer) const {
    return make_shadow_effect(jstyle, *fBuilder, std::move(layer),
                              ShadowAdapter::Type::kDropShadow);
}

sk_sp<sksg::RenderNode> EffectBuilder::attachInnerShadowStyle(const skjson::ObjectValue& jstyle,
                                                              sk_sp<sksg::RenderNode> layer) const {
    return make_shadow_effect(jstyle, *fBuilder, std::move(layer),
                              ShadowAdapter::Type::kInnerShadow);
}

}  // namespace skottie::internal
