/*
 * 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/SkColor.h"
#include "include/core/SkMatrix.h"
#include "include/core/SkRefCnt.h"
#include "include/core/SkScalar.h"
#include "include/private/base/SkAssert.h"
#include "include/private/base/SkDebug.h"
#include "include/private/base/SkFloatingPoint.h"
#include "include/private/base/SkPoint_impl.h"
#include "include/private/base/SkTPin.h"
#include "include/private/base/SkTo.h"
#include "modules/skottie/src/SkottieJson.h"
#include "modules/skottie/src/SkottieValue.h"
#include "modules/skottie/src/animator/Animator.h"
#include "modules/skottie/src/layers/shapelayer/ShapeLayer.h"
#include "modules/sksg/include/SkSGGradient.h"
#include "modules/sksg/include/SkSGPaint.h"
#include "modules/sksg/include/SkSGRenderEffect.h"
#include "src/utils/SkJSON.h"

#include <algorithm>
#include <cstddef>
#include <utility>
#include <vector>

namespace skottie {
namespace internal {
class AnimationBuilder;

namespace  {

class GradientAdapter final : public AnimatablePropertyContainer {
public:
    static sk_sp<GradientAdapter> Make(const skjson::ObjectValue& jgrad,
                                       const AnimationBuilder& abuilder) {
        const skjson::ObjectValue* jstops = jgrad["g"];
        if (!jstops)
            return nullptr;

        const auto stopCount = ParseDefault<int>((*jstops)["p"], -1);
        if (stopCount < 0)
            return nullptr;

        const auto type = (ParseDefault<int>(jgrad["t"], 1) == 1) ? Type::kLinear
                                                                  : Type::kRadial;
        auto gradient_node = (type == Type::kLinear)
                ? sk_sp<sksg::Gradient>(sksg::LinearGradient::Make())
                : sk_sp<sksg::Gradient>(sksg::RadialGradient::Make());

        return sk_sp<GradientAdapter>(new GradientAdapter(std::move(gradient_node),
                                                          type,
                                                          SkToSizeT(stopCount),
                                                          jgrad, *jstops, abuilder));
    }

    const sk_sp<sksg::Gradient>& node() const { return fGradient; }

private:
    enum class Type { kLinear, kRadial };

    GradientAdapter(sk_sp<sksg::Gradient> gradient,
                    Type type,
                    size_t stop_count,
                    const skjson::ObjectValue& jgrad,
                    const skjson::ObjectValue& jstops,
                    const AnimationBuilder& abuilder)
        : fGradient(std::move(gradient))
        , fType(type)
        , fStopCount(stop_count) {
        this->bind(abuilder,  jgrad["s"], fStartPoint     );
        this->bind(abuilder,  jgrad["e"], fEndPoint       );
        this->bind(abuilder,  jgrad["h"], fHighlightLength);
        this->bind(abuilder,  jgrad["a"], fHighlightAngle );
        this->bind(abuilder, jstops["k"], fStops          );
    }

    void onSync() override {
        const auto s_point = SkPoint{fStartPoint.x, fStartPoint.y},
                   e_point = SkPoint{  fEndPoint.x,   fEndPoint.y};

        switch (fType) {
        case Type::kLinear: {
            auto* grad = static_cast<sksg::LinearGradient*>(fGradient.get());
            grad->setStartPoint(s_point);
            grad->setEndPoint(e_point);

            break;
        }
        case Type::kRadial: {
            // The highlight parameters control the location of the actual gradient start point
            // (equivalent to SVG's radial gradient focal point
            //  https://www.w3.org/TR/SVG11/pservers.html#RadialGradients)
            //
            //   - highlight length determines the position along the |s_point -> e_point| vector,
            //     where 0% corresponds to s_point and 100% corresponds to e_point.
            //   - highlight angle rotates the point around s_point
            //
            const SkPoint rotated_e_point =
                    SkMatrix::RotateDeg(fHighlightAngle, s_point).mapPoint(e_point);

            // The valid range for length is [-100% .. 100%], where negative values mirror the
            // positive interval relative to s_point.
            //
            // Edge case: for exactly -100% and 100%, the focal point lies on the end circle and
            // this triggers specific SVG behavior (see
            // SkConicalGrdient::FocalData::isFocalOnCircle and friends), which does not match
            // AE's semantics.  To avoid that, we clamp by an epsilon value.
            const float eps = SK_ScalarNearlyZero * 2,
                      h_len = SkTPin(fHighlightLength * 0.01f, -1 + eps, 1 - eps);

            const SkPoint focal_point = s_point + (rotated_e_point - s_point) * h_len;

            auto* grad = static_cast<sksg::RadialGradient*>(fGradient.get());
            grad->setStartCenter(focal_point);
            grad->setEndCenter(s_point);
            grad->setStartRadius(0);
            grad->setEndRadius(SkPoint::Distance(s_point, rotated_e_point));

            break;
        }
        }

        // Gradient color stops are specified as a consolidated float vector holding:
        //
        //   a) an (optional) array of color/RGB stop records (t, r, g, b)
        //
        // followed by
        //
        //   b) an (optional) array of opacity/alpha stop records (t, a)
        //
        struct   ColorRec { float t, r, g, b; };
        struct OpacityRec { float t, a;       };

        // The number of color records is explicit (fColorStopCount),
        // while the number of opacity stops is implicit (based on the size of fStops).
        //
        // |fStops| holds ColorRec x |fColorStopCount| + OpacityRec x N
        const auto c_count = fStopCount,
                   c_size  = c_count * 4,
                   o_count = (fStops.size() - c_size) / 2;
        if (fStops.size() < c_size || fStops.size() != (c_count * 4 + o_count * 2)) {
            // apply() may get called before the stops are set, so only log when we have some stops.
            if (!fStops.empty()) {
                SkDebugf("!! Invalid gradient stop array size: %zu\n", fStops.size());
            }
            return;
        }

        const auto* c_rec = c_count > 0
                ? reinterpret_cast<const ColorRec*>(fStops.data())
                : nullptr;
        const auto* o_rec = o_count > 0
                ? reinterpret_cast<const OpacityRec*>(fStops.data() + c_size)
                : nullptr;
        const auto* c_end = c_rec + c_count;
        const auto* o_end = o_rec + o_count;

        sksg::Gradient::ColorStop current_stop = {
            0.0f, {
                c_rec ? c_rec->r : 0,
                c_rec ? c_rec->g : 0,
                c_rec ? c_rec->b : 0,
                o_rec ? o_rec->a : 1,
        }};

        std::vector<sksg::Gradient::ColorStop> stops;
        stops.reserve(c_count);

        // Merge-sort the color and opacity stops, LERP-ing intermediate channel values as needed.
        while (c_rec || o_rec) {
            // After exhausting one of color recs / opacity recs, continue propagating the last
            // computed values (as if they were specified at the current position).
            const auto& cs = c_rec
                    ? *c_rec
                    : ColorRec{ o_rec->t,
                                current_stop.fColor.fR,
                                current_stop.fColor.fG,
                                current_stop.fColor.fB };
            const auto& os = o_rec
                    ? *o_rec
                    : OpacityRec{ c_rec->t, current_stop.fColor.fA };

            // Compute component lerp coefficients based on the relative position of the stops
            // being considered. The idea is to select the smaller-pos stop, use its own properties
            // as specified (lerp with t == 1), and lerp (with t < 1) the properties from the
            // larger-pos stop against the previously computed gradient stop values.
            const auto     c_pos = std::max(cs.t, current_stop.fPosition),
                           o_pos = std::max(os.t, current_stop.fPosition),
                       c_pos_rel = c_pos - current_stop.fPosition,
                       o_pos_rel = o_pos - current_stop.fPosition,
                             t_c = SkTPin(sk_ieee_float_divide(o_pos_rel, c_pos_rel), 0.0f, 1.0f),
                             t_o = SkTPin(sk_ieee_float_divide(c_pos_rel, o_pos_rel), 0.0f, 1.0f);

            auto lerp = [](float a, float b, float t) { return a + t * (b - a); };

            current_stop = {
                    std::min(c_pos, o_pos),
                    {
                        lerp(current_stop.fColor.fR, cs.r, t_c ),
                        lerp(current_stop.fColor.fG, cs.g, t_c ),
                        lerp(current_stop.fColor.fB, cs.b, t_c ),
                        lerp(current_stop.fColor.fA, os.a, t_o)
                    }
            };
            stops.push_back(current_stop);

            // Consume one of, or both (for coincident positions) color/opacity stops.
            if (c_pos <= o_pos) {
                c_rec = next_rec<ColorRec>(c_rec, c_end);
            }
            if (o_pos <= c_pos) {
                o_rec = next_rec<OpacityRec>(o_rec, o_end);
            }
        }

        stops.shrink_to_fit();
        fGradient->setColorStops(std::move(stops));
    }

private:
    template <typename T>
    const T* next_rec(const T* rec, const T* end_rec) const {
        if (!rec) return nullptr;

        SkASSERT(rec < end_rec);
        rec++;

        return rec < end_rec ? rec : nullptr;
    }

    const sk_sp<sksg::Gradient> fGradient;
    const Type                  fType;
    const size_t                fStopCount;

    VectorValue  fStops;
    Vec2Value    fStartPoint = {0,0},
                 fEndPoint   = {0,0};
    float        fHighlightLength = 0,
                 fHighlightAngle  = 0;
};

} // namespace

sk_sp<sksg::PaintNode> ShapeBuilder::AttachGradientFill(const skjson::ObjectValue& jgrad,
                                                        const AnimationBuilder* abuilder) {
    auto adapter = GradientAdapter::Make(jgrad, *abuilder);

    return adapter
            ? AttachFill(jgrad, abuilder, sksg::ShaderPaint::Make(adapter->node()), adapter)
            : nullptr;
}

sk_sp<sksg::PaintNode> ShapeBuilder::AttachGradientStroke(const skjson::ObjectValue& jgrad,
                                                          const AnimationBuilder* abuilder) {
    auto adapter = GradientAdapter::Make(jgrad, *abuilder);

    return adapter
            ? AttachStroke(jgrad, abuilder, sksg::ShaderPaint::Make(adapter->node()), adapter)
            : nullptr;
}

} // namespace internal
} // namespace skottie
