/*
 * Copyright 2018 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/SkCanvas.h"
#include "include/core/SkPoint.h"
#include "include/core/SkRect.h"
#include "include/core/SkRefCnt.h"
#include "include/core/SkScalar.h"
#include "include/core/SkSize.h"
#include "include/private/base/SkAssert.h"
#include "include/private/base/SkFloatingPoint.h"
#include "modules/jsonreader/SkJSONReader.h"
#include "modules/skottie/include/ExternalLayer.h"
#include "modules/skottie/include/SkottieProperty.h"
#include "modules/skottie/src/Composition.h"
#include "modules/skottie/src/SkottieJson.h"
#include "modules/skottie/src/SkottiePriv.h"
#include "modules/skottie/src/SkottieValue.h"
#include "modules/skottie/src/animator/Animator.h"
#include "modules/sksg/include/SkSGNode.h"
#include "modules/sksg/include/SkSGRenderNode.h"

#include <cmath>
#include <utility>

class SkMatrix;

namespace sksg {
class InvalidationController;
}

namespace skottie {
namespace internal {

namespace  {

// "Animates" time based on the layer's "tm" property.
class TimeRemapper final : public AnimatablePropertyContainer {
public:
    TimeRemapper(const skjson::ObjectValue& jtm, const AnimationBuilder* abuilder, float scale)
        : fScale(scale) {
        this->bind(*abuilder, jtm, fT);
    }

    float t() const { return fT * fScale; }

private:
    void onSync() override {
        // nothing to sync - we just track t
    }

    const float fScale;

    ScalarValue fT = 0;
};

// Applies a bias/scale/remap t-adjustment to child animators.
class CompTimeMapper final : public Animator {
public:
    CompTimeMapper(AnimatorScope&& layer_animators,
                   sk_sp<TimeRemapper> remapper,
                   float time_bias, float time_scale)
        : fAnimators(std::move(layer_animators))
        , fRemapper(std::move(remapper))
        , fTimeBias(time_bias)
        , fTimeScale(time_scale) {}

    StateChanged onSeek(float t) override {
        if (fRemapper) {
            // When time remapping is active, |t| is fully driven externally.
            fRemapper->seek(t);
            t = fRemapper->t();
        } else {
            t = (t + fTimeBias) * fTimeScale;
        }

        bool changed = false;

        for (const auto& anim : fAnimators) {
            changed |= anim->seek(t);
        }

        return changed;
    }

private:
    const AnimatorScope       fAnimators;
    const sk_sp<TimeRemapper> fRemapper;
    const float               fTimeBias,
                              fTimeScale;
};

} // namespace

sk_sp<sksg::RenderNode> AnimationBuilder::attachExternalPrecompLayer(
        const skjson::ObjectValue& jlayer,
        const LayerInfo& layer_info) const {

    if (!fPrecompInterceptor) {
        return nullptr;
    }

    const skjson::StringValue* id = jlayer["refId"];
    const skjson::StringValue* nm = jlayer["nm"];

    if (!id || !nm) {
        return nullptr;
    }

    auto external_layer = fPrecompInterceptor->onLoadPrecomp(id->begin(),
                                                             nm->begin(),
                                                             layer_info.fSize);
    if (!external_layer) {
        return nullptr;
    }

    // Attaches an ExternalLayer implementation to the animation scene graph.
    class SGAdapter final : public sksg::RenderNode {
    public:
        SG_ATTRIBUTE(T, float, fCurrentT)

        SGAdapter(sk_sp<ExternalLayer> external, const SkSize& layer_size)
            : fExternal(std::move(external))
            , fSize(layer_size) {}

    private:
        SkRect onRevalidate(sksg::InvalidationController*, const SkMatrix&) override {
            return SkRect::MakeSize(fSize);
        }

        void onRender(SkCanvas* canvas, const RenderContext* ctx) const override {
            // Commit all pending effects via a layer if needed,
            // since we don't have knowledge of the external content.
            const auto local_scope =
                ScopedRenderContext(canvas, ctx).setIsolation(this->bounds(),
                                                              canvas->getTotalMatrix(),
                                                              true);
            fExternal->render(canvas, static_cast<double>(fCurrentT));
        }

        const RenderNode* onNodeAt(const SkPoint& pt) const override {
            SkASSERT(this->bounds().contains(pt.fX, pt.fY));
            return this;
        }

        const sk_sp<ExternalLayer> fExternal;
        const SkSize               fSize;
        float                      fCurrentT = 0;
    };

    // Connects an SGAdapter to the animator tree and dispatches seek events.
    class AnimatorAdapter final : public Animator {
    public:
        AnimatorAdapter(sk_sp<SGAdapter> sg_adapter, float fps)
            : fSGAdapter(std::move(sg_adapter))
            , fFps(fps) {}

    private:
        StateChanged onSeek(float t) override {
            fSGAdapter->setT(t / fFps);

            return true;
        }

        const sk_sp<SGAdapter> fSGAdapter;
        const float            fFps;
    };

    auto sg_adapter = sk_make_sp<SGAdapter>(std::move(external_layer), layer_info.fSize);

    fCurrentAnimatorScope->push_back(sk_make_sp<AnimatorAdapter>(sg_adapter, fFrameRate));

    return sg_adapter;
}

sk_sp<sksg::RenderNode> AnimationBuilder::attachPrecompLayer(const skjson::ObjectValue& jlayer,
                                                             LayerInfo* layer_info) const {
    sk_sp<TimeRemapper> time_remapper;
    if (const skjson::ObjectValue* jtm = jlayer["tm"]) {
        time_remapper = sk_make_sp<TimeRemapper>(*jtm, this, fFrameRate);
    }

    const auto start_time = ParseDefault<float>(jlayer["st"], 0.0f),
             stretch_time = ParseDefault<float>(jlayer["sr"], 1.0f);
    const auto requires_time_mapping = !SkScalarNearlyEqual(start_time  , 0) ||
                                       !SkScalarNearlyEqual(stretch_time, 1) ||
                                       time_remapper;

    // Precomp layers are sized explicitly.
    auto parse_size = [](const skjson::ObjectValue& jlayer) {
        return SkSize::Make(ParseDefault<float>(jlayer["w"], 0.0f),
                            ParseDefault<float>(jlayer["h"], 0.0f));
    };
    layer_info->fSize = parse_size(jlayer);

    std::optional<AutoScope> local_scope;
    if (requires_time_mapping) {
        local_scope.emplace(this);
    }

    auto precomp_layer = this->attachExternalPrecompLayer(jlayer, *layer_info);

    if (!precomp_layer) {
        const ScopedAssetRef precomp_asset(this, jlayer);
        if (precomp_asset) {
            // Unlike regular precomp layers, glyph precomps don't have an explicit size - they
            // use the actual asset comp size.
            if (layer_info->fSize.isEmpty()) {
                layer_info->fSize = parse_size(*precomp_asset);
            }

            AutoPropertyTracker apt(this, *precomp_asset, PropertyObserver::NodeType::COMPOSITION);
            precomp_layer =
                CompositionBuilder(*this, layer_info->fSize, *precomp_asset).build(*this);
        }
    }

    if (requires_time_mapping) {
        const auto t_bias  = -start_time,
                   t_scale = sk_ieee_float_divide(1, stretch_time);
        auto time_mapper = sk_make_sp<CompTimeMapper>(local_scope->release(),
                                                      std::move(time_remapper),
                                                      t_bias,
                                                      std::isfinite(t_scale) ? t_scale : 0);

        fCurrentAnimatorScope->push_back(std::move(time_mapper));
    }

    return precomp_layer;
}

} // namespace internal
} // namespace skottie
