/*
 * Copyright 2021 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 "include/core/SkCanvas.h"
#include "include/core/SkM44.h"
#include "include/core/SkPicture.h"
#include "include/core/SkPictureRecorder.h"
#include "include/core/SkTileMode.h"
#include "include/effects/SkRuntimeEffect.h"
#include "modules/skottie/src/Adapter.h"
#include "modules/skottie/src/SkottieJson.h"
#include "modules/skottie/src/SkottieValue.h"
#include "modules/sksg/include/SkSGRenderNode.h"

#include <array>

namespace skottie::internal {

#ifdef SK_ENABLE_SKSL

namespace  {

// This shader maps its child shader onto a sphere.  To simplify things, we set it up such that:
//
//   - the sphere is centered at origin and has r == 1
//   - the eye is positioned at (0,0,eye_z), where eye_z is chosen to visually match AE
//   - the POI for a given pixel is on the z = 0 plane (x,y,0)
//   - we're only rendering inside the projected circle, which guarantees a quadratic solution
//
// Effect stages:
//
//   1) ray-cast to find the sphere intersection (selectable front/back solution);
//      given the sphere geometry, this is also the normal
//   2) rotate the normal
//   3) UV-map the sphere
//   4) scale uv to source size and sample
//   5) apply lighting model
//
// Note: the current implementation uses two passes for two-side ("full") rendering, on the
//       assumption that in practice most textures are opaque and two-side mode is infrequent;
//       if this proves to be problematic, we could expand the implementation to blend both sides
//       in one pass.
//
static constexpr char gSphereSkSL[] =
    "uniform shader child;"

    "uniform half3x3 rot_matrix;"
    "uniform half2 child_scale;"
    "uniform half side_select;"

    // apply_light()
    "%s"

    "half3 to_sphere(half3 EYE) {"
        "half eye_z2 = EYE.z*EYE.z;"

        "half a = dot(EYE, EYE),"
             "b = -2*eye_z2,"
             "c = eye_z2 - 1,"
             "t = (-b + side_select*sqrt(b*b - 4*a*c))/(2*a);"

        "return half3(0, 0, -EYE.z) + EYE*t;"
    "}"

    "half4 main(float2 xy) {"
        "half3 EYE = half3(xy, -5.5),"
                "N = to_sphere(EYE),"
               "RN = rot_matrix*N;"

        "half kRPI = 1/3.1415927;"

        "half2 UV = half2("
            "0.5 + kRPI * 0.5 * atan(RN.x, RN.z),"
            "0.5 + kRPI * asin(RN.y)"
        ");"

        "return apply_light(EYE, N, child.eval(UV*child_scale));"
    "}"
;

// CC Sphere uses a Phong-like lighting model:
//
//   - "ambient" controls the intensity of the texture color
//   - "diffuse" controls a multiplicative mix of texture and light color
//   - "specular" controls a light color specular component
//   - "roughness" is the specular exponent reciprocal
//   - "light intensity" modulates the diffuse and specular components (but not ambient)
//   - "light height" and "light direction" specify the light source position in spherical coords
//
// Implementation-wise, light intensity/height/direction are all combined into l_vec.
// For efficiency, we fall back to a stripped-down shader (ambient-only) when the diffuse & specular
// components are not used.
//
// TODO: "metal" and "reflective" parameters are ignored.
static constexpr char gBasicLightSkSL[] =
    "uniform half l_coeff_ambient;"

    "half4 apply_light(half3 EYE, half3 N, half4 c) {"
        "c.rgb *= l_coeff_ambient;"
        "return c;"
    "}"
;

static constexpr char gFancyLightSkSL[] =
    "uniform half3 l_vec;"
    "uniform half3 l_color;"
    "uniform half l_coeff_ambient;"
    "uniform half l_coeff_diffuse;"
    "uniform half l_coeff_specular;"
    "uniform half l_specular_exp;"

    "half4 apply_light(half3 EYE, half3 N, half4 c) {"
        "half3 LR = reflect(-l_vec*side_select, N);"
        "half s_base = max(dot(normalize(EYE), LR), 0),"

        "a = l_coeff_ambient,"
        "d = l_coeff_diffuse * max(dot(l_vec, N), 0),"
        "s = l_coeff_specular * saturate(pow(s_base, l_specular_exp));"

        "c.rgb = (a + d*l_color)*c.rgb + s*l_color*c.a;"

        "return c;"
    "}"
;

static sk_sp<SkRuntimeEffect> sphere_fancylight_effect() {
    static const SkRuntimeEffect* effect =
            SkRuntimeEffect::MakeForShader(SkStringPrintf(gSphereSkSL, gFancyLightSkSL), {})
                    .effect.release();
    if (0 && !effect) {
        printf("!!! %s\n",
               SkRuntimeEffect::MakeForShader(SkStringPrintf(gSphereSkSL, gFancyLightSkSL), {})
                       .errorText.c_str());
    }
    SkASSERT(effect);

    return sk_ref_sp(effect);
}

static sk_sp<SkRuntimeEffect> sphere_basiclight_effect() {
    static const SkRuntimeEffect* effect =
            SkRuntimeEffect::MakeForShader(SkStringPrintf(gSphereSkSL, gBasicLightSkSL), {})
                    .effect.release();
    SkASSERT(effect);

    return sk_ref_sp(effect);
}

class SphereNode final : public sksg::CustomRenderNode {
public:
    SphereNode(sk_sp<RenderNode> child, const SkSize& child_size)
        : INHERITED({std::move(child)})
        , fChildSize(child_size) {}

    enum class RenderSide {
        kFull,
        kOutside,
        kInside,
    };

    SG_ATTRIBUTE(Center  , SkPoint   , fCenter)
    SG_ATTRIBUTE(Radius  , float     , fRadius)
    SG_ATTRIBUTE(Rotation, SkM44     , fRot   )
    SG_ATTRIBUTE(Side    , RenderSide, fSide  )

    SG_ATTRIBUTE(LightVec     , SkV3 , fLightVec     )
    SG_ATTRIBUTE(LightColor   , SkV3 , fLightColor   )
    SG_ATTRIBUTE(AmbientLight , float, fAmbientLight )
    SG_ATTRIBUTE(DiffuseLight , float, fDiffuseLight )
    SG_ATTRIBUTE(SpecularLight, float, fSpecularLight)
    SG_ATTRIBUTE(SpecularExp  , float, fSpecularExp  )

private:
    sk_sp<SkShader> contentShader() {
        if (!fContentShader || this->hasChildrenInval()) {
            const auto& child = this->children()[0];
            child->revalidate(nullptr, SkMatrix::I());

            SkPictureRecorder recorder;
            child->render(recorder.beginRecording(SkRect::MakeSize(fChildSize)));

            fContentShader = recorder.finishRecordingAsPicture()
                    ->makeShader(SkTileMode::kRepeat, SkTileMode::kRepeat, SkFilterMode::kLinear,
                                 nullptr, nullptr);
        }

        return fContentShader;
    }

    sk_sp<SkShader> buildEffectShader(float selector) {
        const auto has_fancy_light =
                fLightVec.length() > 0 && (fDiffuseLight > 0 || fSpecularLight > 0);

        SkRuntimeShaderBuilder builder(has_fancy_light
                                           ? sphere_fancylight_effect()
                                           : sphere_basiclight_effect());

        builder.child  ("child")       = this->contentShader();
        builder.uniform("child_scale") = fChildSize;
        builder.uniform("side_select") = selector;
        builder.uniform("rot_matrix")  = std::array<float,9>{
            fRot.rc(0,0), fRot.rc(0,1), fRot.rc(0,2),
            fRot.rc(1,0), fRot.rc(1,1), fRot.rc(1,2),
            fRot.rc(2,0), fRot.rc(2,1), fRot.rc(2,2),
        };

        builder.uniform("l_coeff_ambient")  = fAmbientLight;

        if (has_fancy_light) {
            builder.uniform("l_vec")            = fLightVec * -selector;
            builder.uniform("l_color")          = fLightColor;
            builder.uniform("l_coeff_diffuse")  = fDiffuseLight;
            builder.uniform("l_coeff_specular") = fSpecularLight;
            builder.uniform("l_specular_exp")   = fSpecularExp;
        }

        const auto lm = SkMatrix::Translate(fCenter.fX, fCenter.fY) *
                        SkMatrix::Scale(fRadius, fRadius);

        return builder.makeShader(&lm);
    }

    SkRect onRevalidate(sksg::InvalidationController* ic, const SkMatrix& ctm) override {
        fSphereShader.reset();
        if (fSide != RenderSide::kOutside) {
            fSphereShader = this->buildEffectShader(1);
        }
        if (fSide != RenderSide::kInside) {
            auto outside = this->buildEffectShader(-1);
            fSphereShader = fSphereShader
                    ? SkShaders::Blend(SkBlendMode::kSrcOver,
                                       std::move(fSphereShader),
                                       std::move(outside))
                    : std::move(outside);
        }
        SkASSERT(fSphereShader);

        return SkRect::MakeLTRB(fCenter.fX - fRadius,
                                fCenter.fY - fRadius,
                                fCenter.fX + fRadius,
                                fCenter.fY + fRadius);
    }

    void onRender(SkCanvas* canvas, const RenderContext* ctx) const override {
        if (fRadius <= 0) {
            return;
        }

        SkPaint sphere_paint;
        sphere_paint.setAntiAlias(true);
        sphere_paint.setShader(fSphereShader);

        canvas->drawCircle(fCenter, fRadius, sphere_paint);
    }

    const RenderNode* onNodeAt(const SkPoint&) const override { return nullptr; } // no hit-testing

    const SkSize fChildSize;

    // Cached shaders
    sk_sp<SkShader> fSphereShader;
    sk_sp<SkShader> fContentShader;

    // Effect controls.
    SkM44      fRot;
    SkPoint    fCenter = {0,0};
    float      fRadius = 0;
    RenderSide fSide   = RenderSide::kFull;

    SkV3       fLightVec      = {0,0,1},
               fLightColor    = {1,1,1};
    float      fAmbientLight  = 1,
               fDiffuseLight  = 0,
               fSpecularLight = 0,
               fSpecularExp   = 0;

    using INHERITED = sksg::CustomRenderNode;
};

class SphereAdapter final : public DiscardableAdapterBase<SphereAdapter, SphereNode> {
public:
    SphereAdapter(const skjson::ArrayValue& jprops,
                  const AnimationBuilder* abuilder,
                  sk_sp<SphereNode> node)
        : INHERITED(std::move(node))
    {
        enum : size_t {
            //      kRotGrp_Index =  0,
                      kRotX_Index =  1,
                      kRotY_Index =  2,
                      kRotZ_Index =  3,
                  kRotOrder_Index =  4,
            // ???                =  5,
                    kRadius_Index =  6,
                    kOffset_Index =  7,
                    kRender_Index =  8,

            //       kLight_Index =  9,
            kLightIntensity_Index = 10,
                kLightColor_Index = 11,
               kLightHeight_Index = 12,
            kLightDirection_Index = 13,
            // ???                = 14,
            //     kShading_Index = 15,
                   kAmbient_Index = 16,
                   kDiffuse_Index = 17,
                  kSpecular_Index = 18,
                 kRoughness_Index = 19,
        };

        EffectBinder(jprops, *abuilder, this)
            .bind(  kOffset_Index, fOffset  )
            .bind(  kRadius_Index, fRadius  )
            .bind(    kRotX_Index, fRotX    )
            .bind(    kRotY_Index, fRotY    )
            .bind(    kRotZ_Index, fRotZ    )
            .bind(kRotOrder_Index, fRotOrder)
            .bind(  kRender_Index, fRender  )

            .bind(kLightIntensity_Index, fLightIntensity)
            .bind(    kLightColor_Index, fLightColor    )
            .bind(   kLightHeight_Index, fLightHeight   )
            .bind(kLightDirection_Index, fLightDirection)
            .bind(       kAmbient_Index, fAmbient       )
            .bind(       kDiffuse_Index, fDiffuse       )
            .bind(      kSpecular_Index, fSpecular      )
            .bind(     kRoughness_Index, fRoughness     );
    }

private:
    void onSync() override {
        const auto side = [](ScalarValue s) {
            switch (SkScalarRoundToInt(s)) {
                case 1:  return SphereNode::RenderSide::kFull;
                case 2:  return SphereNode::RenderSide::kOutside;
                case 3:
                default: return SphereNode::RenderSide::kInside;
            }
            SkUNREACHABLE;
        };

        const auto rotation = [](ScalarValue order,
                                 ScalarValue x, ScalarValue y, ScalarValue z) {
            const SkM44 rx = SkM44::Rotate({1,0,0}, SkDegreesToRadians( x)),
                        ry = SkM44::Rotate({0,1,0}, SkDegreesToRadians( y)),
                        rz = SkM44::Rotate({0,0,1}, SkDegreesToRadians(-z));

            switch (SkScalarRoundToInt(order)) {
                case 1: return rx * ry * rz;
                case 2: return rx * rz * ry;
                case 3: return ry * rx * rz;
                case 4: return ry * rz * rx;
                case 5: return rz * rx * ry;
                case 6:
               default: return rz * ry * rx;
            }
            SkUNREACHABLE;
        };

        const auto light_vec = [](float height, float direction) {
            float z = std::sin(height * SK_ScalarPI / 2),
                  r = std::sqrt(1 - z*z),
                  x = std::cos(direction) * r,
                  y = std::sin(direction) * r;

            return SkV3{x,y,z};
        };

        const auto& sph = this->node();

        sph->setCenter({fOffset.x, fOffset.y});
        sph->setRadius(fRadius);
        sph->setSide(side(fRender));
        sph->setRotation(rotation(fRotOrder, fRotX, fRotY, fRotZ));

        sph->setAmbientLight (SkTPin(fAmbient * 0.01f, 0.0f, 2.0f));

        const auto intensity = SkTPin(fLightIntensity * 0.01f,  0.0f, 10.0f);
        sph->setDiffuseLight (SkTPin(fDiffuse * 0.01f, 0.0f, 1.0f) * intensity);
        sph->setSpecularLight(SkTPin(fSpecular* 0.01f, 0.0f, 1.0f) * intensity);

        sph->setLightVec(light_vec(
            SkTPin(fLightHeight    * 0.01f, -1.0f,  1.0f),
            SkDegreesToRadians(fLightDirection - 90)
        ));

        const auto lc = static_cast<SkColor4f>(fLightColor);
        sph->setLightColor({lc.fR, lc.fG, lc.fB});

        sph->setSpecularExp(1/SkTPin(fRoughness, 0.001f, 0.5f));
    }

    Vec2Value   fOffset   = {0,0};
    ScalarValue fRadius   = 0,
                fRotX     = 0,
                fRotY     = 0,
                fRotZ     = 0,
                fRotOrder = 1,
                fRender   = 1;

    ColorValue  fLightColor;
    ScalarValue fLightIntensity =   0,
                fLightHeight    =   0,
                fLightDirection =   0,
                fAmbient        = 100,
                fDiffuse        =   0,
                fSpecular       =   0,
                fRoughness      =   0.5f;

    using INHERITED = DiscardableAdapterBase<SphereAdapter, SphereNode>;
};

} // namespace

#endif  // SK_ENABLE_SKSL

sk_sp<sksg::RenderNode> EffectBuilder::attachSphereEffect(
        const skjson::ArrayValue& jprops, sk_sp<sksg::RenderNode> layer) const {
#ifdef SK_ENABLE_SKSL
    auto sphere = sk_make_sp<SphereNode>(std::move(layer), fLayerSize);

    return fBuilder->attachDiscardableAdapter<SphereAdapter>(jprops, fBuilder, std::move(sphere));
#else
    // TODO(skia:12197)
    return layer;
#endif
}

} // namespace skottie::internal
