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

#include "modules/skottie/src/effects/Effects.h"

#include "modules/skottie/include/Skottie.h"
#include "modules/skottie/include/SkottieProperty.h"
#include "modules/skottie/src/SkottieJson.h"
#include "modules/skottie/src/SkottiePriv.h"
#include "modules/sksg/include/SkSGRenderEffect.h"
#include "modules/sksg/include/SkSGRenderNode.h"

#include <algorithm>
#include <cstdint>
#include <cstring>
#include <iterator>
#include <limits>
#include <utility>

namespace skottie {
namespace internal {

EffectBuilder::EffectBuilder(const AnimationBuilder* abuilder,
                             const SkSize& layer_size,
                             CompositionBuilder* cbuilder)
    : fBuilder(abuilder)
    , fCompBuilder(cbuilder)
    , fLayerSize(layer_size) {}

EffectBuilder::EffectBuilderT EffectBuilder::findBuilder(const skjson::ObjectValue& jeffect) const {
    static constexpr struct BuilderInfo {
        const char*    fName;
        EffectBuilderT fBuilder;
    } gBuilderInfo[] = {
        // alphabetized for binary search lookup
        { "ADBE Black&White"            , &EffectBuilder::attachBlackAndWhiteEffect      },
        { "ADBE Brightness & Contrast 2", &EffectBuilder::attachBrightnessContrastEffect },
        { "ADBE Bulge"                  , &EffectBuilder::attachBulgeEffect              },
        { "ADBE Corner Pin"             , &EffectBuilder::attachCornerPinEffect          },
        { "ADBE Displacement Map"       , &EffectBuilder::attachDisplacementMapEffect    },
        { "ADBE Drop Shadow"            , &EffectBuilder::attachDropShadowEffect         },
        { "ADBE Easy Levels2"           , &EffectBuilder::attachEasyLevelsEffect         },
        { "ADBE Fill"                   , &EffectBuilder::attachFillEffect               },
        { "ADBE Fractal Noise"          , &EffectBuilder::attachFractalNoiseEffect       },
        { "ADBE Gaussian Blur 2"        , &EffectBuilder::attachGaussianBlurEffect       },
        { "ADBE Geometry2"              , &EffectBuilder::attachTransformEffect          },
        { "ADBE HUE SATURATION"         , &EffectBuilder::attachHueSaturationEffect      },
        { "ADBE Invert"                 , &EffectBuilder::attachInvertEffect             },
        { "ADBE Linear Wipe"            , &EffectBuilder::attachLinearWipeEffect         },
        { "ADBE Motion Blur"            , &EffectBuilder::attachDirectionalBlurEffect    },
        { "ADBE Pro Levels2"            , &EffectBuilder::attachProLevelsEffect          },
        { "ADBE Radial Wipe"            , &EffectBuilder::attachRadialWipeEffect         },
        { "ADBE Ramp"                   , &EffectBuilder::attachGradientEffect           },
        { "ADBE Sharpen"                , &EffectBuilder::attachSharpenEffect            },
        { "ADBE Shift Channels"         , &EffectBuilder::attachShiftChannelsEffect      },
        { "ADBE Threshold2"             , &EffectBuilder::attachThresholdEffect          },
        { "ADBE Tile"                   , &EffectBuilder::attachMotionTileEffect         },
        { "ADBE Tint"                   , &EffectBuilder::attachTintEffect               },
        { "ADBE Tritone"                , &EffectBuilder::attachTritoneEffect            },
        { "ADBE Venetian Blinds"        , &EffectBuilder::attachVenetianBlindsEffect     },
        { "CC Sphere"                   , &EffectBuilder::attachSphereEffect             },
        { "CC Toner"                    , &EffectBuilder::attachCCTonerEffect            },
        { "SkSL Color Filter"           , &EffectBuilder::attachSkSLColorFilter          },
        { "SkSL Shader"                 , &EffectBuilder::attachSkSLShader               },
    };

    const skjson::StringValue* mn = jeffect["mn"];
    if (mn) {
        const BuilderInfo key { mn->begin(), nullptr };
        const auto* binfo = std::lower_bound(std::begin(gBuilderInfo),
                                             std::end  (gBuilderInfo),
                                             key,
                                             [](const BuilderInfo& a, const BuilderInfo& b) {
                                                 return strcmp(a.fName, b.fName) < 0;
                                             });
        if (binfo != std::end(gBuilderInfo) && !strcmp(binfo->fName, key.fName)) {
            return binfo->fBuilder;
        }
    }

    // Some legacy clients rely solely on the 'ty' field and generate (non-BM) JSON
    // without a valid 'mn' string.  TODO: we should update them and remove this fallback.
    enum : int32_t {
        kTint_Effect         = 20,
        kFill_Effect         = 21,
        kTritone_Effect      = 23,
        kDropShadow_Effect   = 25,
        kRadialWipe_Effect   = 26,
        kGaussianBlur_Effect = 29,
    };

    switch (ParseDefault<int>(jeffect["ty"], -1)) {
        case         kTint_Effect: return &EffectBuilder::attachTintEffect;
        case         kFill_Effect: return &EffectBuilder::attachFillEffect;
        case      kTritone_Effect: return &EffectBuilder::attachTritoneEffect;
        case   kDropShadow_Effect: return &EffectBuilder::attachDropShadowEffect;
        case   kRadialWipe_Effect: return &EffectBuilder::attachRadialWipeEffect;
        case kGaussianBlur_Effect: return &EffectBuilder::attachGaussianBlurEffect;
        default: break;
    }

    fBuilder->log(Logger::Level::kWarning, &jeffect,
                  "Unsupported layer effect: %s", mn ? mn->begin() : "(unknown)");

    return nullptr;
}

sk_sp<sksg::RenderNode> EffectBuilder::attachEffects(const skjson::ArrayValue& jeffects,
                                                     sk_sp<sksg::RenderNode> layer) const {
    if (!layer) {
        return nullptr;
    }

    for (const skjson::ObjectValue* jeffect : jeffects) {
        if (!jeffect) {
            continue;
        }

        const auto builder = this->findBuilder(*jeffect);
        const skjson::ArrayValue* jprops = (*jeffect)["ef"];
        if (!builder || !jprops) {
            continue;
        }

        const AnimationBuilder::AutoPropertyTracker apt(fBuilder, *jeffect, PropertyObserver::NodeType::EFFECT);
        layer = (this->*builder)(*jprops, std::move(layer));

        if (!layer) {
            fBuilder->log(Logger::Level::kError, jeffect, "Invalid layer effect.");
            return nullptr;
        }
    }

    return layer;
}

sk_sp<sksg::RenderNode> EffectBuilder::attachStyles(const skjson::ArrayValue& jstyles,
                                                     sk_sp<sksg::RenderNode> layer) const {
#if !defined(SKOTTIE_DISABLE_STYLES)
    if (!layer) {
        return nullptr;
    }

    using StyleBuilder =
        sk_sp<sksg::RenderNode> (EffectBuilder::*)(const skjson::ObjectValue&,
                                                   sk_sp<sksg::RenderNode>) const;
    static constexpr StyleBuilder gStyleBuilders[] = {
        nullptr,                                 // 'ty': 0 -> stroke
        &EffectBuilder::attachDropShadowStyle,   // 'ty': 1 -> drop shadow
        &EffectBuilder::attachInnerShadowStyle,  // 'ty': 2 -> inner shadow
        &EffectBuilder::attachOuterGlowStyle,    // 'ty': 3 -> outer glow
        &EffectBuilder::attachInnerGlowStyle,    // 'ty': 4 -> inner glow
    };

    for (const skjson::ObjectValue* jstyle : jstyles) {
        if (!jstyle) {
            continue;
        }

        const auto style_type =
                ParseDefault<size_t>((*jstyle)["ty"], std::numeric_limits<size_t>::max());
        auto builder = style_type < std::size(gStyleBuilders) ? gStyleBuilders[style_type]
                                                              : nullptr;

        if (!builder) {
            fBuilder->log(Logger::Level::kWarning, jstyle, "Unsupported layer style.");
            continue;
        }

        layer = (this->*builder)(*jstyle, std::move(layer));
    }
#endif // !defined(SKOTTIE_DISABLE_STYLES)

    return layer;
}

const skjson::Value& EffectBuilder::GetPropValue(const skjson::ArrayValue& jprops,
                                                 size_t prop_index) {
    static skjson::NullValue kNull;

    if (prop_index >= jprops.size()) {
        return kNull;
    }

    const skjson::ObjectValue* jprop = jprops[prop_index];

    return jprop ? (*jprop)["v"] : kNull;
}

MaskShaderEffectBase::MaskShaderEffectBase(sk_sp<sksg::RenderNode> child, const SkSize& ls)
    : fMaskEffectNode(sksg::MaskShaderEffect::Make(std::move(child)))
    , fLayerSize(ls) {}

void MaskShaderEffectBase::onSync() {
    const auto minfo = this->onMakeMask();

    fMaskEffectNode->setVisible(minfo.fVisible);
    fMaskEffectNode->setShader(std::move(minfo.fMaskShader));
}

} // namespace internal
} // namespace skottie
