/*
 * 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.
 */

package android.view;

import static junit.framework.Assert.assertFalse;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import android.app.Activity;
import android.content.Context;
import android.graphics.Rect;
import android.support.test.InstrumentationRegistry;
import android.support.test.annotation.UiThreadTest;
import android.support.test.filters.LargeTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.widget.FrameLayout;

import com.android.compatibility.common.util.WidgetTestUtils;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

/**
 * Test of invalidates, drawing, and the flags that support them
 */
@LargeTest
@RunWith(AndroidJUnit4.class)
public class ViewInvalidateTest {
    @Rule
    public ActivityTestRule<Activity> mActivityRule = new ActivityTestRule<>(Activity.class);

    private static final int INVAL_TEST_FLAG_MASK = View.PFLAG_DIRTY
            | View.PFLAG_DIRTY_OPAQUE
            | View.PFLAG_DRAWN
            | View.PFLAG_DRAWING_CACHE_VALID
            | View.PFLAG_INVALIDATED
            | View.PFLAG_DRAW_ANIMATION;

    @Before
    public void setup() throws Throwable {
        // separate runnable to initialize, so ref is safe to pass to runOnMainAndDrawSync
        mActivityRule.runOnUiThread(() -> {
            mParent = new FrameLayout(getContext());
            mChild = new View(getContext());
        });

        // attached view is drawn once
        WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mParent, () -> {
            mParent.addView(mChild);
            getActivity().setContentView(mParent);

            // 'invalidated', but not yet drawn
            validateInvalFlags(mChild, View.PFLAG_INVALIDATED);
        });
    }

    @After
    public void teardown() {
        // ensure we don't share views between tests
        mParent = null;
        mChild = null;
    }

    Context getContext() {
        return InstrumentationRegistry.getTargetContext();
    }

    Activity getActivity() {
        return mActivityRule.getActivity();
    }

    private ViewGroup mParent;
    private View mChild;

    private static void validateInvalFlags(View view, int... expectedFlagArray) {
        int expectedFlags = 0;
        for (int expectedFlag : expectedFlagArray) {
            expectedFlags |= expectedFlag;
        }

        final int observedFlags = view.mPrivateFlags & INVAL_TEST_FLAG_MASK;
        assertEquals(String.format("expect %x, observed %x", expectedFlags, observedFlags),
                expectedFlags, observedFlags);
    }

    private static ViewRootImpl getViewRoot(View view) {
        ViewParent parent = view.getParent();
        while (parent != null) {
            if (parent instanceof ViewRootImpl) {
                return (ViewRootImpl) parent;
            }
            parent = parent.getParent();
        }
        return null;
    }

    @UiThreadTest
    @Test
    public void testInvalidate_behavior() throws Throwable {
        validateInvalFlags(mChild,
                View.PFLAG_DRAWING_CACHE_VALID,
                View.PFLAG_DRAWN);
        validateInvalFlags(mParent,
                View.PFLAG_DRAWING_CACHE_VALID,
                View.PFLAG_DRAWN);
        assertFalse(getViewRoot(mParent).mTraversalScheduled);

        mChild.invalidate();

        // no longer drawn, is now invalidated
        validateInvalFlags(mChild,
                View.PFLAG_DIRTY,
                View.PFLAG_INVALIDATED);

        // parent drawing cache no longer valid, marked dirty
        validateInvalFlags(mParent,
                View.PFLAG_DRAWN,
                View.PFLAG_DIRTY);
        assertTrue(getViewRoot(mParent).mTraversalScheduled);
    }

    @UiThreadTest
    @Test
    public void testInvalidate_false() {
        // Invalidate makes it invalid
        validateInvalFlags(mChild,
                View.PFLAG_DRAWING_CACHE_VALID,
                View.PFLAG_DRAWN);

        mChild.invalidate(/*don't invalidate cache*/ false);

        // drawn is cleared, dirty set, nothing else changed
        validateInvalFlags(mChild,
                View.PFLAG_DRAWING_CACHE_VALID,
                View.PFLAG_DIRTY);
    }

    @Test
    public void testInvalidate_simple() throws Throwable {
        // simple invalidate, which marks the view invalid
        WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mParent, () -> {
            validateInvalFlags(mChild,
                    View.PFLAG_DRAWING_CACHE_VALID,
                    View.PFLAG_DRAWN);

            mChild.invalidate();

            validateInvalFlags(mChild,
                    View.PFLAG_DIRTY,
                    View.PFLAG_INVALIDATED);
        });

        // after draw pass, view has drawn, no longer invalid
        mActivityRule.runOnUiThread(() -> {
            validateInvalFlags(mChild,
                    View.PFLAG_DRAWING_CACHE_VALID,
                    View.PFLAG_DRAWN);
        });
    }

    @UiThreadTest
    @Test
    public void testInvalidate_manualUpdateDisplayList() {
        // Invalidate makes it invalid
        validateInvalFlags(mChild,
                View.PFLAG_DRAWING_CACHE_VALID,
                View.PFLAG_DRAWN);

        mChild.invalidate();
        validateInvalFlags(mChild,
                View.PFLAG_DIRTY,
                View.PFLAG_INVALIDATED);

        // updateDisplayListIfDirty makes it valid again, but invalidate still set,
        // since it's cleared by View#draw(canvas, parent, drawtime)
        mChild.updateDisplayListIfDirty();
            validateInvalFlags(mChild,
                    View.PFLAG_DRAWING_CACHE_VALID,
                    View.PFLAG_DRAWN,
                    View.PFLAG_INVALIDATED);
    }

    @UiThreadTest
    @Test
    public void testInvalidateChild_simple() {
        validateInvalFlags(mParent,
                View.PFLAG_DRAWING_CACHE_VALID,
                View.PFLAG_DRAWN);
        assertFalse(getViewRoot(mParent).mTraversalScheduled);

        mParent.invalidateChild(mChild, new Rect(0, 0, 1, 1));

        validateInvalFlags(mParent,
                View.PFLAG_DIRTY,
                View.PFLAG_DRAWN);
        assertTrue(getViewRoot(mParent).mTraversalScheduled);
    }

    @Test
    public void testInvalidateChild_childHardwareLayer() throws Throwable {
        WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mParent, () -> {
            // do in runnable, so tree won't be dirty
            mParent.setLayerType(View.LAYER_TYPE_HARDWARE, null);
        });

        mActivityRule.runOnUiThread(() -> {
            validateInvalFlags(mParent,
                    View.PFLAG_DRAWING_CACHE_VALID,
                    View.PFLAG_DRAWN);

            mParent.invalidateChild(mChild, new Rect(0, 0, 1, 1));

            validateInvalFlags(mParent,
                    View.PFLAG_DIRTY,
                    View.PFLAG_DRAWN); // Note: note invalidated, since HW damage handled in native
        });
    }

    @Test
    public void testInvalidateChild_childSoftwareLayer() throws Throwable {
        WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mParent, () -> {
            // do in runnable, so tree won't be dirty
            mParent.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
        });

        mActivityRule.runOnUiThread(() -> {
            validateInvalFlags(mParent,
                    View.PFLAG_DRAWING_CACHE_VALID,
                    View.PFLAG_DRAWN);

            mParent.invalidateChild(mChild, new Rect(0, 0, 1, 1));

            validateInvalFlags(mParent,
                    View.PFLAG_DIRTY,
                    View.PFLAG_DRAWN,
                    View.PFLAG_INVALIDATED); // Note: invalidated, since SW damage handled here
        });
    }

    @UiThreadTest
    @Test
    public void testInvalidateChild_legacyAnimation() throws Throwable {
        mChild.mPrivateFlags |= View.PFLAG_DRAW_ANIMATION;

        validateInvalFlags(mChild,
                View.PFLAG_DRAW_ANIMATION,
                View.PFLAG_DRAWING_CACHE_VALID,
                View.PFLAG_DRAWN);
        validateInvalFlags(mParent,
                View.PFLAG_DRAWING_CACHE_VALID,
                View.PFLAG_DRAWN);
        assertFalse(getViewRoot(mParent).mIsAnimating);

        mParent.invalidateChild(mChild, new Rect(0, 0, 1, 1));

        validateInvalFlags(mChild,
                View.PFLAG_DRAW_ANIMATION,
                View.PFLAG_DRAWING_CACHE_VALID,
                View.PFLAG_DRAWN);
        validateInvalFlags(mParent,
                View.PFLAG_DIRTY,
                View.PFLAG_DRAW_ANIMATION, // carried up to parent
                View.PFLAG_DRAWN);
        assertTrue(getViewRoot(mParent).mIsAnimating);
    }
}
