/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <VectorDrawable.h>
#include <gtest/gtest.h>

#include "AnimationContext.h"
#include "DamageAccumulator.h"
#include "IContextFactory.h"
#include "pipeline/skia/GLFunctorDrawable.h"
#include "pipeline/skia/SkiaDisplayList.h"
#include "renderthread/CanvasContext.h"
#include "tests/common/TestContext.h"
#include "tests/common/TestUtils.h"

using namespace android;
using namespace android::uirenderer;
using namespace android::uirenderer::renderthread;
using namespace android::uirenderer::skiapipeline;

TEST(SkiaDisplayList, create) {
    SkiaDisplayList skiaDL;
    ASSERT_TRUE(skiaDL.isEmpty());
    ASSERT_FALSE(skiaDL.mProjectionReceiver);
}

TEST(SkiaDisplayList, reset) {
    std::unique_ptr<SkiaDisplayList> skiaDL;
    {
        SkiaRecordingCanvas canvas{nullptr, 1, 1};
        canvas.drawColor(0, SkBlendMode::kSrc);
        skiaDL.reset(canvas.finishRecording());
    }

    SkCanvas dummyCanvas;
    RenderNodeDrawable drawable(nullptr, &dummyCanvas);
    skiaDL->mChildNodes.emplace_back(nullptr, &dummyCanvas);
    GLFunctorDrawable functorDrawable(nullptr, nullptr, &dummyCanvas);
    skiaDL->mChildFunctors.push_back(&functorDrawable);
    skiaDL->mMutableImages.push_back(nullptr);
    skiaDL->appendVD(nullptr);
    skiaDL->mProjectionReceiver = &drawable;

    ASSERT_FALSE(skiaDL->mChildNodes.empty());
    ASSERT_FALSE(skiaDL->mChildFunctors.empty());
    ASSERT_FALSE(skiaDL->mMutableImages.empty());
    ASSERT_TRUE(skiaDL->hasVectorDrawables());
    ASSERT_FALSE(skiaDL->isEmpty());
    ASSERT_TRUE(skiaDL->mProjectionReceiver);

    skiaDL->reset();

    ASSERT_TRUE(skiaDL->mChildNodes.empty());
    ASSERT_TRUE(skiaDL->mChildFunctors.empty());
    ASSERT_TRUE(skiaDL->mMutableImages.empty());
    ASSERT_FALSE(skiaDL->hasVectorDrawables());
    ASSERT_TRUE(skiaDL->isEmpty());
    ASSERT_FALSE(skiaDL->mProjectionReceiver);
}

TEST(SkiaDisplayList, reuseDisplayList) {
    sp<RenderNode> renderNode = new RenderNode();
    std::unique_ptr<SkiaDisplayList> availableList;

    // no list has been attached so it should return a nullptr
    availableList = renderNode->detachAvailableList();
    ASSERT_EQ(availableList.get(), nullptr);

    // attach a displayList for reuse
    SkiaDisplayList skiaDL;
    ASSERT_TRUE(skiaDL.reuseDisplayList(renderNode.get(), nullptr));

    // detach the list that you just attempted to reuse
    availableList = renderNode->detachAvailableList();
    ASSERT_EQ(availableList.get(), &skiaDL);
    availableList.release();  // prevents an invalid free since our DL is stack allocated

    // after detaching there should return no available list
    availableList = renderNode->detachAvailableList();
    ASSERT_EQ(availableList.get(), nullptr);
}

TEST(SkiaDisplayList, syncContexts) {
    SkiaDisplayList skiaDL;

    SkCanvas dummyCanvas;
    TestUtils::MockFunctor functor;
    GLFunctorDrawable functorDrawable(&functor, nullptr, &dummyCanvas);
    skiaDL.mChildFunctors.push_back(&functorDrawable);

    int functor2 = WebViewFunctor_create(
            nullptr, TestUtils::createMockFunctor(RenderMode::OpenGL_ES), RenderMode::OpenGL_ES);
    auto& counts = TestUtils::countsForFunctor(functor2);
    skiaDL.mChildFunctors.push_back(
            skiaDL.allocateDrawable<GLFunctorDrawable>(functor2, &dummyCanvas));
    WebViewFunctor_release(functor2);

    SkRect bounds = SkRect::MakeWH(200, 200);
    VectorDrawableRoot vectorDrawable(new VectorDrawable::Group());
    vectorDrawable.mutateStagingProperties()->setBounds(bounds);
    skiaDL.appendVD(&vectorDrawable);

    // ensure that the functor and vectorDrawable are properly synced
    TestUtils::runOnRenderThread([&](auto&) {
        skiaDL.syncContents(WebViewSyncData{
                .applyForceDark = false,
        });
    });

    EXPECT_EQ(functor.getLastMode(), DrawGlInfo::kModeSync);
    EXPECT_EQ(counts.sync, 1);
    EXPECT_EQ(counts.destroyed, 0);
    EXPECT_EQ(vectorDrawable.mutateProperties()->getBounds(), bounds);

    skiaDL.reset();
    TestUtils::runOnRenderThread([](auto&) {
        // Fence
    });
    EXPECT_EQ(counts.destroyed, 1);
}

class ContextFactory : public IContextFactory {
public:
    virtual AnimationContext* createAnimationContext(renderthread::TimeLord& clock) override {
        return new AnimationContext(clock);
    }
};

RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaDisplayList, prepareListAndChildren) {
    auto rootNode = TestUtils::createNode(0, 0, 200, 400, nullptr);
    ContextFactory contextFactory;
    std::unique_ptr<CanvasContext> canvasContext(
            CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory));
    TreeInfo info(TreeInfo::MODE_FULL, *canvasContext.get());
    DamageAccumulator damageAccumulator;
    info.damageAccumulator = &damageAccumulator;

    SkiaDisplayList skiaDL;

    // The VectorDrawableRoot needs to have bounds on screen (and therefore not
    // empty) in order to have PropertyChangeWillBeConsumed set.
    const auto bounds = SkRect::MakeIWH(100, 100);

    // prepare with a clean VD
    VectorDrawableRoot cleanVD(new VectorDrawable::Group());
    cleanVD.mutateProperties()->setBounds(bounds);
    skiaDL.appendVD(&cleanVD);
    cleanVD.getBitmapUpdateIfDirty();  // this clears the dirty bit

    ASSERT_FALSE(cleanVD.isDirty());
    ASSERT_FALSE(cleanVD.getPropertyChangeWillBeConsumed());
    TestUtils::MockTreeObserver observer;
    ASSERT_FALSE(skiaDL.prepareListAndChildren(observer, info, false,
                                               [](RenderNode*, TreeObserver&, TreeInfo&, bool) {}));
    ASSERT_FALSE(cleanVD.getPropertyChangeWillBeConsumed());

    // prepare again this time adding a dirty VD
    VectorDrawableRoot dirtyVD(new VectorDrawable::Group());
    dirtyVD.mutateProperties()->setBounds(bounds);
    skiaDL.appendVD(&dirtyVD);

    ASSERT_TRUE(dirtyVD.isDirty());
    ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());
    ASSERT_TRUE(skiaDL.prepareListAndChildren(observer, info, false,
                                              [](RenderNode*, TreeObserver&, TreeInfo&, bool) {}));
    ASSERT_TRUE(dirtyVD.getPropertyChangeWillBeConsumed());

    // prepare again this time adding a RenderNode and a callback
    sp<RenderNode> renderNode = new RenderNode();
    TreeInfo* infoPtr = &info;
    SkCanvas dummyCanvas;
    skiaDL.mChildNodes.emplace_back(renderNode.get(), &dummyCanvas);
    bool hasRun = false;
    ASSERT_TRUE(skiaDL.prepareListAndChildren(
            observer, info, false,
            [&hasRun, renderNode, infoPtr](RenderNode* n, TreeObserver& observer, TreeInfo& i,
                                           bool r) {
                hasRun = true;
                ASSERT_EQ(renderNode.get(), n);
                ASSERT_EQ(infoPtr, &i);
                ASSERT_FALSE(r);
            }));
    ASSERT_TRUE(hasRun);

    canvasContext->destroy();
}

RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaDisplayList, prepareListAndChildren_vdOffscreen) {
    auto rootNode = TestUtils::createNode(0, 0, 200, 400, nullptr);
    ContextFactory contextFactory;
    std::unique_ptr<CanvasContext> canvasContext(
            CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory));

    // Set up a Surface so that we can position the VectorDrawable offscreen.
    test::TestContext testContext;
    testContext.setRenderOffscreen(true);
    auto surface = testContext.surface();
    int width, height;
    surface->query(NATIVE_WINDOW_WIDTH, &width);
    surface->query(NATIVE_WINDOW_HEIGHT, &height);
    canvasContext->setSurface(std::move(surface));

    TreeInfo info(TreeInfo::MODE_FULL, *canvasContext.get());
    DamageAccumulator damageAccumulator;
    info.damageAccumulator = &damageAccumulator;

    // The VectorDrawableRoot needs to have bounds on screen (and therefore not
    // empty) in order to have PropertyChangeWillBeConsumed set.
    const auto bounds = SkRect::MakeIWH(100, 100);

    for (const SkRect b : {bounds.makeOffset(width, 0),
                           bounds.makeOffset(0, height),
                           bounds.makeOffset(-bounds.width(), 0),
                           bounds.makeOffset(0, -bounds.height())}) {
        SkiaDisplayList skiaDL;
        VectorDrawableRoot dirtyVD(new VectorDrawable::Group());
        dirtyVD.mutateProperties()->setBounds(b);
        skiaDL.appendVD(&dirtyVD);

        ASSERT_TRUE(dirtyVD.isDirty());
        ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());

        TestUtils::MockTreeObserver observer;
        ASSERT_FALSE(skiaDL.prepareListAndChildren(
                observer, info, false, [](RenderNode*, TreeObserver&, TreeInfo&, bool) {}));
        ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());
    }

    // The DamageAccumulator's transform can also result in the
    // VectorDrawableRoot being offscreen.
    for (const SkISize translate : { SkISize{width, 0},
                                     SkISize{0, height},
                                     SkISize{-width, 0},
                                     SkISize{0, -height}}) {
        Matrix4 mat4;
        mat4.translate(translate.fWidth, translate.fHeight);
        damageAccumulator.pushTransform(&mat4);

        SkiaDisplayList skiaDL;
        VectorDrawableRoot dirtyVD(new VectorDrawable::Group());
        dirtyVD.mutateProperties()->setBounds(bounds);
        skiaDL.appendVD(&dirtyVD);

        ASSERT_TRUE(dirtyVD.isDirty());
        ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());

        TestUtils::MockTreeObserver observer;
        ASSERT_FALSE(skiaDL.prepareListAndChildren(
                observer, info, false, [](RenderNode*, TreeObserver&, TreeInfo&, bool) {}));
        ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());
        damageAccumulator.popTransform();
    }

    // Another way to be offscreen: a matrix from the draw call.
    for (const SkMatrix translate : { SkMatrix::MakeTrans(width, 0),
                                      SkMatrix::MakeTrans(0, height),
                                      SkMatrix::MakeTrans(-width, 0),
                                      SkMatrix::MakeTrans(0, -height)}) {
        SkiaDisplayList skiaDL;
        VectorDrawableRoot dirtyVD(new VectorDrawable::Group());
        dirtyVD.mutateProperties()->setBounds(bounds);
        skiaDL.appendVD(&dirtyVD, translate);

        ASSERT_TRUE(dirtyVD.isDirty());
        ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());

        TestUtils::MockTreeObserver observer;
        ASSERT_FALSE(skiaDL.prepareListAndChildren(
                observer, info, false, [](RenderNode*, TreeObserver&, TreeInfo&, bool) {}));
        ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());
    }

    // Verify that the matrices are combined in the right order.
    {
        // Rotate and then translate, so the VD is offscreen.
        Matrix4 mat4;
        mat4.loadRotate(180);
        damageAccumulator.pushTransform(&mat4);

        SkiaDisplayList skiaDL;
        VectorDrawableRoot dirtyVD(new VectorDrawable::Group());
        dirtyVD.mutateProperties()->setBounds(bounds);
        SkMatrix translate = SkMatrix::MakeTrans(50, 50);
        skiaDL.appendVD(&dirtyVD, translate);

        ASSERT_TRUE(dirtyVD.isDirty());
        ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());

        TestUtils::MockTreeObserver observer;
        ASSERT_FALSE(skiaDL.prepareListAndChildren(
                observer, info, false, [](RenderNode*, TreeObserver&, TreeInfo&, bool) {}));
        ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());
        damageAccumulator.popTransform();
    }
    {
        // Switch the order of rotate and translate, so it is on screen.
        Matrix4 mat4;
        mat4.translate(50, 50);
        damageAccumulator.pushTransform(&mat4);

        SkiaDisplayList skiaDL;
        VectorDrawableRoot dirtyVD(new VectorDrawable::Group());
        dirtyVD.mutateProperties()->setBounds(bounds);
        SkMatrix rotate;
        rotate.setRotate(180);
        skiaDL.appendVD(&dirtyVD, rotate);

        ASSERT_TRUE(dirtyVD.isDirty());
        ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());

        TestUtils::MockTreeObserver observer;
        ASSERT_TRUE(skiaDL.prepareListAndChildren(
                observer, info, false, [](RenderNode*, TreeObserver&, TreeInfo&, bool) {}));
        ASSERT_TRUE(dirtyVD.getPropertyChangeWillBeConsumed());
        damageAccumulator.popTransform();
    }
    {
        // An AVD that is larger than the screen.
        SkiaDisplayList skiaDL;
        VectorDrawableRoot dirtyVD(new VectorDrawable::Group());
        dirtyVD.mutateProperties()->setBounds(SkRect::MakeLTRB(-1, -1, width + 1, height + 1));
        skiaDL.appendVD(&dirtyVD);

        ASSERT_TRUE(dirtyVD.isDirty());
        ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());

        TestUtils::MockTreeObserver observer;
        ASSERT_TRUE(skiaDL.prepareListAndChildren(
                observer, info, false, [](RenderNode*, TreeObserver&, TreeInfo&, bool) {}));
        ASSERT_TRUE(dirtyVD.getPropertyChangeWillBeConsumed());
    }
    {
        // An AVD whose bounds are not a rectangle after applying a matrix.
        SkiaDisplayList skiaDL;
        VectorDrawableRoot dirtyVD(new VectorDrawable::Group());
        dirtyVD.mutateProperties()->setBounds(bounds);
        SkMatrix mat;
        mat.setRotate(45, 50, 50);
        skiaDL.appendVD(&dirtyVD, mat);

        ASSERT_TRUE(dirtyVD.isDirty());
        ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());

        TestUtils::MockTreeObserver observer;
        ASSERT_TRUE(skiaDL.prepareListAndChildren(
                observer, info, false, [](RenderNode*, TreeObserver&, TreeInfo&, bool) {}));
        ASSERT_TRUE(dirtyVD.getPropertyChangeWillBeConsumed());
    }
}

TEST(SkiaDisplayList, updateChildren) {
    SkiaDisplayList skiaDL;

    sp<RenderNode> renderNode = new RenderNode();
    SkCanvas dummyCanvas;
    skiaDL.mChildNodes.emplace_back(renderNode.get(), &dummyCanvas);
    skiaDL.updateChildren([renderNode](RenderNode* n) { ASSERT_EQ(renderNode.get(), n); });
}