/*
 * 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/SkContourMeasure.h"
#include "include/core/SkPathBuilder.h"
#include "modules/skottie/src/SkottieJson.h"
#include "modules/skottie/src/SkottieValue.h"
#include "modules/skottie/src/animator/Animator.h"
#include "modules/skottie/src/animator/KeyframeAnimator.h"

#include <cmath>

namespace skottie::internal {

namespace  {

// Spatial 2D specialization: stores SkV2s and optional contour interpolators externally.
class Vec2KeyframeAnimator final : public KeyframeAnimator {
    struct SpatialValue {
        Vec2Value               v2;
        sk_sp<SkContourMeasure> cmeasure;
    };

public:
    class Builder final : public KeyframeAnimatorBuilder {
    public:
        Builder(Vec2Value* vec_target, float* rot_target)
            : fVecTarget(vec_target)
            , fRotTarget(rot_target) {}

        sk_sp<KeyframeAnimator> make(const AnimationBuilder& abuilder,
                                     const skjson::ArrayValue& jkfs) override {
            SkASSERT(jkfs.size() > 0);

            fValues.reserve(jkfs.size());
            if (!this->parseKeyframes(abuilder, jkfs)) {
                return nullptr;
            }
            fValues.shrink_to_fit();

            return sk_sp<Vec2KeyframeAnimator>(
                        new Vec2KeyframeAnimator(std::move(fKFs),
                                                 std::move(fCMs),
                                                 std::move(fValues),
                                                 fVecTarget,
                                                 fRotTarget));
        }

        bool parseValue(const AnimationBuilder&, const skjson::Value& jv) const override {
            return Parse(jv, fVecTarget);
        }

    private:
        void backfill_spatial(const SpatialValue& val) {
            SkASSERT(!fValues.empty());
            auto& prev_val = fValues.back();
            SkASSERT(!prev_val.cmeasure);

            if (val.v2 == prev_val.v2) {
                // spatial interpolation only make sense for noncoincident values
                return;
            }

            // Check whether v0 and v1 have the same direction AND ||v0||>=||v1||
            auto check_vecs = [](const SkV2& v0, const SkV2& v1) {
                const auto v0_len2 = v0.lengthSquared(),
                           v1_len2 = v1.lengthSquared();

                // check magnitude
                if (v0_len2 < v1_len2) {
                    return false;
                }

                // v0, v1 have the same direction iff dot(v0,v1) = ||v0||*||v1||
                // <=>    dot(v0,v1)^2 = ||v0||^2 * ||v1||^2
                const auto dot = v0.dot(v1);
                return SkScalarNearlyEqual(dot * dot, v0_len2 * v1_len2);
            };

            if (check_vecs(val.v2 - prev_val.v2, fTo) &&
                check_vecs(prev_val.v2 - val.v2, fTi)) {
                // Both control points lie on the [prev_val..val] segment
                //   => we can power-reduce the Bezier "curve" to a straight line.
                return;
            }

            // Finally, this looks like a legitimate spatial keyframe.
            SkPathBuilder p;
            p.moveTo (prev_val.v2.x        , prev_val.v2.y);
            p.cubicTo(prev_val.v2.x + fTo.x, prev_val.v2.y + fTo.y,
                           val.v2.x + fTi.x,      val.v2.y + fTi.y,
                           val.v2.x,              val.v2.y);
            prev_val.cmeasure = SkContourMeasureIter(p.detach(), false).next();
        }

        bool parseKFValue(const AnimationBuilder&,
                          const skjson::ObjectValue& jkf,
                          const skjson::Value& jv,
                          Keyframe::Value* v) override {
            SpatialValue val;
            if (!Parse(jv, &val.v2)) {
                return false;
            }

            if (fPendingSpatial) {
                this->backfill_spatial(val);
            }

            // Track the last keyframe spatial tangents (checked on next parseValue).
            fTi             = ParseDefault<SkV2>(jkf["ti"], {0,0});
            fTo             = ParseDefault<SkV2>(jkf["to"], {0,0});
            fPendingSpatial = fTi != SkV2{0,0} || fTo != SkV2{0,0};

            if (fValues.empty() || val.v2 != fValues.back().v2 || fPendingSpatial) {
                fValues.push_back(std::move(val));
            }

            v->idx = SkToU32(fValues.size() - 1);

            return true;
        }

        std::vector<SpatialValue> fValues;
        Vec2Value*                fVecTarget; // required
        float*                    fRotTarget; // optional
        SkV2                      fTi{0,0},
                                  fTo{0,0};
        bool                      fPendingSpatial = false;
    };

private:
    Vec2KeyframeAnimator(std::vector<Keyframe> kfs, std::vector<SkCubicMap> cms,
                         std::vector<SpatialValue> vs, Vec2Value* vec_target, float* rot_target)
        : INHERITED(std::move(kfs), std::move(cms))
        , fValues(std::move(vs))
        , fVecTarget(vec_target)
        , fRotTarget(rot_target) {}

    StateChanged update(const Vec2Value& new_vec_value, const Vec2Value& new_tan_value) {
        auto changed = (new_vec_value != *fVecTarget);
        *fVecTarget = new_vec_value;

        if (fRotTarget) {
            const auto new_rot_value = SkRadiansToDegrees(std::atan2(new_tan_value.y,
                                                                     new_tan_value.x));
            changed |= new_rot_value != *fRotTarget;
            *fRotTarget = new_rot_value;
        }

        return changed;
    }

    StateChanged onSeek(float t) override {
        auto get_lerp_info = [this](float t) {
            auto lerp_info = this->getLERPInfo(t);

            // When tracking rotation/orientation, the last keyframe requires special handling:
            // it doesn't store any spatial information but it is expected to maintain the
            // previous orientation (per AE semantics).
            //
            // The easiest way to achieve this is to actually swap with the previous keyframe,
            // with an adjusted weight of 1.
            const auto vidx = lerp_info.vrec0.idx;
            if (fRotTarget && vidx == fValues.size() - 1 && vidx > 0) {
                SkASSERT(!fValues[vidx].cmeasure);
                SkASSERT(lerp_info.vrec1.idx == vidx);

                // Change LERPInfo{0, SIZE - 1, SIZE - 1}
                // to     LERPInfo{1, SIZE - 2, SIZE - 1}
                lerp_info.weight = 1;
                lerp_info.vrec0  = {vidx - 1};

                // This yields equivalent lerp results because keyframed values are contiguous
                // i.e frame[n-1].end_val == frame[n].start_val.
            }

            return lerp_info;
        };

        const auto lerp_info = get_lerp_info(t);

        const auto& v0 = fValues[lerp_info.vrec0.idx];
        if (v0.cmeasure) {
            // Spatial keyframe: the computed weight is relative to the interpolation path
            // arc length.
            SkPoint  pos;
            SkVector tan;
            if (v0.cmeasure->getPosTan(lerp_info.weight * v0.cmeasure->length(), &pos, &tan)) {
                return this->update({ pos.fX, pos.fY }, {tan.fX, tan.fY});
            }
        }

        const auto& v1 = fValues[lerp_info.vrec1.idx];
        const auto tan = v1.v2 - v0.v2;

        return this->update(Lerp(v0.v2, v1.v2, lerp_info.weight), tan);
    }

    const std::vector<SpatialValue> fValues;
    Vec2Value*                      fVecTarget;
    float*                          fRotTarget;

    using INHERITED = KeyframeAnimator;
};

} // namespace

bool AnimatablePropertyContainer::bindAutoOrientable(const AnimationBuilder& abuilder,
                                                     const skjson::ObjectValue* jprop,
                                                     Vec2Value* v, float* orientation) {
    if (!jprop) {
        return false;
    }

    if (!ParseDefault<bool>((*jprop)["s"], false)) {
        // Regular (static or keyframed) 2D value.
        Vec2KeyframeAnimator::Builder builder(v, orientation);
        return this->bindImpl(abuilder, jprop, builder);
    }

    // Separate-dimensions vector value: each component is animated independently.
    return this->bind(abuilder, (*jprop)["x"], &v->x)
         | this->bind(abuilder, (*jprop)["y"], &v->y);
}

template <>
bool AnimatablePropertyContainer::bind<Vec2Value>(const AnimationBuilder& abuilder,
                                                  const skjson::ObjectValue* jprop,
                                                  Vec2Value* v) {
    return this->bindAutoOrientable(abuilder, jprop, v, nullptr);
}

} // namespace skottie::internal
